diff --git a/CMakeLists.txt b/CMakeLists.txt index 8b2ac24..5cd879b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,7 +53,8 @@ add_custom_target(panopainter_modernization_status VERBATIM) add_library(pp_foundation STATIC - src/foundation/binary_stream.cpp) + src/foundation/binary_stream.cpp + src/foundation/parse.cpp) target_include_directories(pp_foundation PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src") diff --git a/CMakePresets.json b/CMakePresets.json index e86aafa..1da1b13 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -22,6 +22,16 @@ "PP_ENABLE_VIDEO": "ON" } }, + { + "name": "platform-headless-base", + "hidden": true, + "inherits": "base", + "cacheVariables": { + "PP_BUILD_APP": "OFF", + "PP_ENABLE_CLOUD": "OFF", + "PP_ENABLE_VIDEO": "OFF" + } + }, { "name": "windows-vs2026-x64", "inherits": "base", @@ -50,7 +60,7 @@ }, { "name": "linux-clang", - "inherits": "base", + "inherits": "platform-headless-base", "displayName": "Linux clang", "generator": "Ninja", "cacheVariables": { @@ -59,28 +69,57 @@ } }, { - "name": "android-arm64", - "inherits": "base", - "displayName": "Android arm64-v8a", + "name": "android-base", + "hidden": true, + "inherits": "platform-headless-base", "generator": "Ninja", + "toolchainFile": "$env{ANDROID_NDK_HOME}/build/cmake/android.toolchain.cmake", "cacheVariables": { - "ANDROID_ABI": "arm64-v8a", - "ANDROID_PLATFORM": "android-26" + "ANDROID_PLATFORM": "android-26", + "ANDROID_STL": "c++_shared", + "PP_ENABLE_VR": "OFF", + "PP_ENABLE_OPENGL": "ON" + } + }, + { + "name": "android-arm64", + "inherits": "android-base", + "displayName": "Android arm64-v8a", + "cacheVariables": { + "ANDROID_ABI": "arm64-v8a" } }, { "name": "android-x64", - "inherits": "base", + "inherits": "android-base", "displayName": "Android x86_64", - "generator": "Ninja", "cacheVariables": { - "ANDROID_ABI": "x86_64", - "ANDROID_PLATFORM": "android-26" + "ANDROID_ABI": "x86_64" + } + }, + { + "name": "android-quest-arm64", + "inherits": "android-base", + "displayName": "Android Quest arm64-v8a", + "cacheVariables": { + "ANDROID_ABI": "arm64-v8a", + "PP_ENABLE_VR": "ON", + "PP_ANDROID_FLAVOR": "quest" + } + }, + { + "name": "android-focus-arm64", + "inherits": "android-base", + "displayName": "Android Focus/Wave arm64-v8a", + "cacheVariables": { + "ANDROID_ABI": "arm64-v8a", + "PP_ENABLE_VR": "ON", + "PP_ANDROID_FLAVOR": "focus" } }, { "name": "emscripten", - "inherits": "base", + "inherits": "platform-headless-base", "displayName": "Emscripten WebGL", "generator": "Ninja", "cacheVariables": { @@ -90,13 +129,13 @@ }, { "name": "macos", - "inherits": "base", + "inherits": "platform-headless-base", "displayName": "macOS", "generator": "Ninja" }, { "name": "ios-device", - "inherits": "base", + "inherits": "platform-headless-base", "displayName": "iOS device", "generator": "Xcode", "cacheVariables": { @@ -106,7 +145,7 @@ }, { "name": "ios-simulator", - "inherits": "base", + "inherits": "platform-headless-base", "displayName": "iOS simulator", "generator": "Xcode", "cacheVariables": { @@ -131,6 +170,22 @@ { "name": "linux-clang", "configurePreset": "linux-clang" + }, + { + "name": "android-arm64", + "configurePreset": "android-arm64" + }, + { + "name": "android-x64", + "configurePreset": "android-x64" + }, + { + "name": "android-quest-arm64", + "configurePreset": "android-quest-arm64" + }, + { + "name": "android-focus-arm64", + "configurePreset": "android-focus-arm64" } ], "testPresets": [ diff --git a/cmake/PanoPainterOptions.cmake b/cmake/PanoPainterOptions.cmake index 861c68b..ca6c0bd 100644 --- a/cmake/PanoPainterOptions.cmake +++ b/cmake/PanoPainterOptions.cmake @@ -14,3 +14,6 @@ option(PP_ENABLE_TSAN "Enable ThreadSanitizer for headless targets where support option(PP_ENABLE_MSVC_ANALYZE "Enable MSVC static analysis." OFF) option(PP_ENABLE_CLANG_TIDY "Enable clang-tidy integration." OFF) option(PP_ENABLE_CPPCHECK "Enable cppcheck integration." OFF) + +set(PP_ANDROID_FLAVOR "standard" CACHE STRING "Android package flavor: standard, quest, or focus.") +set_property(CACHE PP_ANDROID_FLAVOR PROPERTY STRINGS standard quest focus) diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 62de6df..fc86906 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -63,6 +63,8 @@ cmake --build --preset windows-msvc-default --config Debug --target PanoPainter ctest --preset desktop-fast --build-config Debug powershell -ExecutionPolicy Bypass -File scripts\automation\test.ps1 -Preset desktop-fast -Configuration Debug powershell -ExecutionPolicy Bypass -File scripts\automation\build.ps1 -Preset windows-msvc-default -Configuration Debug -Target pano_cli +cmake --preset android-arm64 +cmake --build --preset android-arm64 --target pp_foundation pano_cli pp_foundation_binary_stream_tests pp_foundation_parse_tests ``` Known local toolchain state: @@ -71,6 +73,7 @@ Known local toolchain state: - Local Visual Studio generator selected by CMake: Visual Studio 17 2022 - Android SDK: `C:\Users\omara\AppData\Local\Android\Sdk` - Android NDK: `C:\Users\omara\AppData\Local\Android\Sdk\ndk\29.0.14206865` +- Android arm64 headless configure/build passes through root CMake. - `vcpkg` is not on PATH yet; see DEBT-0007. Known warnings after the current CMake app build: diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 2787d32..b626d8e 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -28,6 +28,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0006 | Open | Modernization | `pano_cli create-document` validates and emits JSON command contracts but does not yet invoke the legacy document/app model | The document model has not been extracted from `Canvas`/`App` yet | `pano_cli create-document --width 64 --height 32 --layers 2`; CTest `pano_cli_create_document_smoke` | Replace command contract implementation with real `pp_document` creation once Phase 4 extracts the document model | | DEBT-0007 | Open | Modernization | `vcpkg.json` exists but CMake is not yet using a validated vcpkg toolchain on this machine | `vcpkg` is not available on PATH and Visual Studio reports manifest mode is disabled | `cmake --preset windows-msvc-default` currently configures with vendored dependencies | Add validated vcpkg toolchain/preset integration for desktop, Android, and Apple triplets | | DEBT-0008 | Open | Modernization | `windows-msvc-default` preset is used for local validation because the VS 2026 generator is not installed here | The target VS 2026 preset must remain, but this machine configures with Visual Studio 17 2022 | `cmake --preset windows-msvc-default`; `ctest --preset desktop-fast --build-config Debug` | Validate `windows-vs2026-x64` on a machine with Visual Studio 2026 installed and make it the default Windows validation preset | +| DEBT-0009 | Open | Modernization | Android root CMake validation currently builds headless targets only, not APK/package variants | Platform app entrypoints still live in legacy Gradle/CMake projects and need Phase 6 alignment | `cmake --preset android-arm64`; `cmake --build --preset android-arm64 --target pp_foundation pano_cli pp_foundation_binary_stream_tests pp_foundation_parse_tests` | Android standard, Quest, and Focus/Wave package targets consume shared component targets and have package smoke commands | ## Closed Debt diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 366b571..c5ca775 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -142,8 +142,9 @@ Goal: make CMake the canonical source list without breaking existing projects. Status: in progress. Root `CMakeLists.txt`, `CMakePresets.json`, and project option targets exist. The Windows desktop app builds through CMake as `PanoPainter`; the raw Visual Studio solution/project files were removed on -2026-05-31 by user decision. Non-Windows platform build files remain during -Phase 6 alignment. +2026-05-31 by user decision. Android arm64 now configures and builds headless +foundation/tool targets through the root CMake/NDK path. Non-Windows platform +app/package files remain during Phase 6 alignment. Implementation tasks: @@ -297,8 +298,10 @@ Gate: Goal: split libraries while keeping current app behavior. Status: started. `pp_foundation` exists with binary stream utilities and -boundary/overread tests. Continue extracting legacy-safe utilities before -moving assets, paint, or document behavior. +boundary/overread tests. It also owns strict decimal `uint32` parsing used by +`pano_cli`, with rejection tests for empty, signed, mixed, and overflowing +input. Continue extracting legacy-safe utilities before moving assets, paint, +or document behavior. Implementation tasks: @@ -498,20 +501,25 @@ Last verified on 2026-05-31: ```powershell cmake --preset windows-msvc-default -cmake --build --preset windows-msvc-default --config Debug --target pp_foundation_tests pano_cli PanoPainter +cmake --build --preset windows-msvc-default --config Debug --target pp_foundation_binary_stream_tests pp_foundation_parse_tests pano_cli PanoPainter ctest --preset desktop-fast --build-config Debug powershell -ExecutionPolicy Bypass -File scripts\automation\test.ps1 -Preset desktop-fast -Configuration Debug powershell -ExecutionPolicy Bypass -File scripts\automation\build.ps1 -Preset windows-msvc-default -Configuration Debug -Target pano_cli +cmake --preset android-arm64 +cmake --build --preset android-arm64 --target pp_foundation pano_cli pp_foundation_binary_stream_tests pp_foundation_parse_tests ``` Results: -- `pp_foundation_tests` passed. +- `pp_foundation_binary_stream_tests` passed. +- `pp_foundation_parse_tests` passed. - `pano_cli_create_document_smoke` passed. - `PanoPainter.exe` built through CMake at `out/build/windows-msvc-default/Debug/PanoPainter.exe`. - PowerShell build/test automation wrappers return JSON summaries and passed local smoke checks. +- Android arm64 configured with NDK 29.0.14206865 and compiled headless + foundation/tool/test targets. - Known remaining warnings: legacy project/vendor diagnostics, Visual Studio vcpkg-manifest warning, `LNK4099` missing libyuv PDBs, and `LNK4098` runtime library conflict from retained vendor binaries. diff --git a/src/foundation/parse.cpp b/src/foundation/parse.cpp new file mode 100644 index 0000000..6a401f6 --- /dev/null +++ b/src/foundation/parse.cpp @@ -0,0 +1,37 @@ +#include "foundation/parse.h" + +#include + +namespace pp::foundation { + +Result parse_u32(std::string_view text) noexcept +{ + if (text.empty()) { + return Result::failure( + Status::invalid_argument("value must not be empty")); + } + + if (text.front() == '-' || text.front() == '+') { + return Result::failure( + Status::invalid_argument("value must be an unsigned integer without a sign")); + } + + std::uint32_t value = 0; + const auto* begin = text.data(); + const auto* end = text.data() + text.size(); + const auto [ptr, ec] = std::from_chars(begin, end, value); + + if (ec == std::errc::result_out_of_range) { + return Result::failure( + Status::out_of_range("value is outside the uint32 range")); + } + + if (ec != std::errc {} || ptr != end) { + return Result::failure( + Status::invalid_argument("value must contain only decimal digits")); + } + + return Result::success(value); +} + +} diff --git a/src/foundation/parse.h b/src/foundation/parse.h new file mode 100644 index 0000000..62aafcf --- /dev/null +++ b/src/foundation/parse.h @@ -0,0 +1,12 @@ +#pragma once + +#include "foundation/result.h" + +#include +#include + +namespace pp::foundation { + +[[nodiscard]] Result parse_u32(std::string_view text) noexcept; + +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b7e8ce4..ee416a0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -5,14 +5,25 @@ target_link_libraries(pp_test_harness INTERFACE pp_project_options pp_project_warnings) -add_executable(pp_foundation_tests - foundation/binary_stream_tests.cpp) -target_link_libraries(pp_foundation_tests PRIVATE +add_executable(pp_foundation_binary_stream_tests + foundation/binary_stream_tests.cpp +) +target_link_libraries(pp_foundation_binary_stream_tests PRIVATE pp_foundation pp_test_harness) -add_test(NAME pp_foundation_tests COMMAND pp_foundation_tests) -set_tests_properties(pp_foundation_tests PROPERTIES +add_test(NAME pp_foundation_binary_stream_tests COMMAND pp_foundation_binary_stream_tests) +set_tests_properties(pp_foundation_binary_stream_tests PROPERTIES + LABELS "foundation;desktop-fast") + +add_executable(pp_foundation_parse_tests + foundation/parse_tests.cpp) +target_link_libraries(pp_foundation_parse_tests PRIVATE + pp_foundation + pp_test_harness) + +add_test(NAME pp_foundation_parse_tests COMMAND pp_foundation_parse_tests) +set_tests_properties(pp_foundation_parse_tests PROPERTIES LABELS "foundation;desktop-fast") if(TARGET pano_cli) diff --git a/tests/foundation/parse_tests.cpp b/tests/foundation/parse_tests.cpp new file mode 100644 index 0000000..dde91b0 --- /dev/null +++ b/tests/foundation/parse_tests.cpp @@ -0,0 +1,66 @@ +#include "foundation/parse.h" +#include "test_harness.h" + +#include +#include + +using pp::foundation::parse_u32; +using pp::foundation::StatusCode; + +namespace { + +void accepts_decimal_uint32_values(pp::tests::Harness& h) +{ + const auto zero = parse_u32("0"); + const auto ordinary = parse_u32("12345"); + const auto max = parse_u32("4294967295"); + + PP_EXPECT(h, zero.ok()); + PP_EXPECT(h, zero.value() == 0U); + PP_EXPECT(h, ordinary.ok()); + PP_EXPECT(h, ordinary.value() == 12345U); + PP_EXPECT(h, max.ok()); + PP_EXPECT(h, max.value() == UINT32_MAX); +} + +void rejects_empty_signed_and_mixed_input(pp::tests::Harness& h) +{ + const auto empty = parse_u32(""); + const auto negative = parse_u32("-1"); + const auto positive = parse_u32("+1"); + const auto trailing = parse_u32("12px"); + const auto spaced = parse_u32(" 12"); + + PP_EXPECT(h, !empty.ok()); + PP_EXPECT(h, empty.status().code == StatusCode::invalid_argument); + PP_EXPECT(h, !negative.ok()); + PP_EXPECT(h, negative.status().code == StatusCode::invalid_argument); + PP_EXPECT(h, !positive.ok()); + PP_EXPECT(h, positive.status().code == StatusCode::invalid_argument); + PP_EXPECT(h, !trailing.ok()); + PP_EXPECT(h, trailing.status().code == StatusCode::invalid_argument); + PP_EXPECT(h, !spaced.ok()); + PP_EXPECT(h, spaced.status().code == StatusCode::invalid_argument); +} + +void rejects_overflow_without_wrapping(pp::tests::Harness& h) +{ + const auto overflow = parse_u32("4294967296"); + const auto very_large = parse_u32("999999999999999999999999999999999999"); + + PP_EXPECT(h, !overflow.ok()); + PP_EXPECT(h, overflow.status().code == StatusCode::out_of_range); + PP_EXPECT(h, !very_large.ok()); + PP_EXPECT(h, very_large.status().code == StatusCode::out_of_range); +} + +} + +int main() +{ + pp::tests::Harness harness; + harness.run("accepts_decimal_uint32_values", accepts_decimal_uint32_values); + harness.run("rejects_empty_signed_and_mixed_input", rejects_empty_signed_and_mixed_input); + harness.run("rejects_overflow_without_wrapping", rejects_overflow_without_wrapping); + return harness.finish(); +} diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 10ad764..11f3fa4 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -1,6 +1,6 @@ +#include "foundation/parse.h" #include "foundation/result.h" -#include #include #include #include @@ -13,14 +13,6 @@ struct DocumentArgs { std::uint32_t layers = 1; }; -bool parse_u32(std::string_view text, std::uint32_t& value) -{ - const auto* begin = text.data(); - const auto* end = text.data() + text.size(); - const auto [ptr, ec] = std::from_chars(begin, end, value); - return ec == std::errc {} && ptr == end; -} - void print_error(std::string_view command, std::string_view message) { std::cout << "{\"ok\":false,\"command\":\"" << command @@ -44,17 +36,17 @@ pp::foundation::Status parse_document_args(int argc, char** argv, DocumentArgs& return pp::foundation::Status::invalid_argument("missing value for option"); } - std::uint32_t value = 0; - if (!parse_u32(argv[++i], value)) { - return pp::foundation::Status::invalid_argument("option value must be an unsigned integer"); + const auto value = pp::foundation::parse_u32(argv[++i]); + if (!value) { + return value.status(); } if (key == "--width") { - args.width = value; + args.width = value.value(); } else if (key == "--height") { - args.height = value; + args.height = value.value(); } else { - args.layers = value; + args.layers = value.value(); } } else { return pp::foundation::Status::invalid_argument("unknown option");