From 20fe7469c43c94c71c76bfc28ef30b62d57349e5 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Sat, 20 Dec 2025 23:13:08 +0100 Subject: [PATCH 1/9] Add initial project structure with CMake configuration, Clang format and tidy settings, GitHub workflows, and integration tests for Windows serial communication bindings --- .clang-format | 161 +++ .clang-tidy | 596 +++++++++++ .github/workflows/build-deno-dll.yml | 43 + .github/workflows/build.yml | 30 - .github/workflows/cpp-tests.yml | 34 + .github/workflows/deno-tests.yml | 44 + .github/workflows/publish-jsr.yml | 65 ++ .gitignore | 37 + CMakeLists.txt | 162 +++ README.md | 2 +- cmake/CPM.cmake | 1363 +++++++++++++++++++++++++ integration_tests/deno.json | 14 + integration_tests/deno.lock | 25 + integration_tests/ffi_bindings.ts | 73 ++ integration_tests/integration_test.ts | 43 + jsr/package/README.md | 21 + jsr/package/jsr.json | 19 + jsr/package/jsr.json.in | 19 + jsr/scripts/embed_binary.ps1 | 40 + jsr/scripts/embed_binary.sh | 35 + src/detail/win32_helpers.hpp | 150 +++ src/main.cpp | 182 ---- src/serial_close.cpp | 39 + src/serial_open.cpp | 164 +++ src/serial_read.cpp | 238 +++++ src/serial_write.cpp | 110 ++ src/test.cpp | 9 - tests/test_serial_arduino.cpp | 160 +++ 28 files changed, 3656 insertions(+), 222 deletions(-) create mode 100644 .clang-format create mode 100644 .clang-tidy create mode 100644 .github/workflows/build-deno-dll.yml delete mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/cpp-tests.yml create mode 100644 .github/workflows/deno-tests.yml create mode 100644 .github/workflows/publish-jsr.yml create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 cmake/CPM.cmake create mode 100644 integration_tests/deno.json create mode 100644 integration_tests/deno.lock create mode 100644 integration_tests/ffi_bindings.ts create mode 100644 integration_tests/integration_test.ts create mode 100644 jsr/package/README.md create mode 100644 jsr/package/jsr.json create mode 100644 jsr/package/jsr.json.in create mode 100644 jsr/scripts/embed_binary.ps1 create mode 100755 jsr/scripts/embed_binary.sh create mode 100644 src/detail/win32_helpers.hpp delete mode 100644 src/main.cpp create mode 100644 src/serial_close.cpp create mode 100644 src/serial_open.cpp create mode 100644 src/serial_read.cpp create mode 100644 src/serial_write.cpp delete mode 100644 src/test.cpp create mode 100644 tests/test_serial_arduino.cpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..ed08e49 --- /dev/null +++ b/.clang-format @@ -0,0 +1,161 @@ +--- +Language: Cpp +# BasedOnStyle: Microsoft +AccessModifierOffset: -2 +AlignAfterOpenBracket: Align +AlignEscapedNewlines: Right +AlignOperands: Align +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: true +AllowAllConstructorInitializersOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortEnumsOnASingleLine: false +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: None +AllowShortLambdasOnASingleLine: All +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: MultiLine +AttributeMacros: + - __capability +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: Always + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: true + AfterStruct: true + AfterUnion: false + AfterExternBlock: true + BeforeCatch: true + BeforeElse: true + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeConceptDeclarations: true +BreakBeforeBraces: Custom +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 120 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DeriveLineEnding: true +DerivePointerAlignment: false +DisableFormat: false +EmptyLineBeforeAccessModifier: LogicalBlock +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +StatementAttributeLikeMacros: + - Q_EMIT +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^"(llvm|llvm-c|clang|clang-c)/' + Priority: 2 + SortPriority: 0 + CaseSensitive: false + - Regex: '^(<|"(gtest|gmock|isl|json)/)' + Priority: 3 + SortPriority: 0 + CaseSensitive: false + - Regex: '.*' + Priority: 1 + SortPriority: 0 + CaseSensitive: false +IncludeIsMainRegex: '(Test)?$' +IncludeIsMainSourceRegex: '' +IndentCaseLabels: false +IndentCaseBlocks: false +IndentGotoLabels: true +IndentPPDirectives: None +IndentExternBlock: AfterExternBlock +IndentRequires: false +IndentWidth: 4 +IndentWrappedFunctionNames: false +InsertTrailingCommas: None +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Auto +ObjCBlockIndentWidth: 2 +ObjCBreakBeforeNestedBlockParam: true +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 1000 +PenaltyIndentedWhitespace: 0 +PointerAlignment: Right +ReflowComments: true +SortIncludes: true +SortJavaStaticImport: Before +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceAroundPointerQualifiers: Default +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInConditionalStatement: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpaceBeforeSquareBrackets: false +BitFieldColonSpacing: Both +Standard: Latest +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TabWidth: 4 +UseCRLF: false +UseTab: Never +WhitespaceSensitiveMacros: + - STRINGIZE + - PP_STRINGIZE + - BOOST_PP_STRINGIZE + - NS_SWIFT_NAME + - CF_SWIFT_NAME +... diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..554459d --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,596 @@ +--- +Checks: '*, + -bugprone-easily-swappable-parameters, + -bugprone-lambda-function-name, + -readability-magic-numbers, + -hicpp-no-array-decay, + -hicpp-signed-bitwise, + -hicpp-vararg, + -misc-non-private-member-variables-in-classes, + -cppcoreguidelines-*, + -fuchsia-*, + -altera-*, + -android-*, + -llvmlibc-*, + -readability-convert-member-functions-to-static, + -boost-use-ranges, + -google-objc-function-naming, + -google-objc-global-variable-declaration, + -performance-enum-size' +WarningsAsErrors: '' +HeaderFilterRegex: '^(?!magic_enum).*$' +FormatStyle: Microsoft +CheckOptions: + - key: readability-identifier-naming.ClassCase + value: CamelCase + - key: readability-identifier-naming.ClassMemberCase + value: lower_case + - key: readability-identifier-naming.ConstexprVariableCase + value: CamelCase + - key: readability-identifier-naming.ConstexprVariablePrefix + value: k + - key: readability-identifier-naming.EnumCase + value: CamelCase + - key: readability-identifier-naming.EnumConstantCase + value: CamelCase + - key: readability-identifier-naming.EnumConstantPrefix + value: k + - key: readability-identifier-naming.FunctionCase + value: camelBack + - key: readability-identifier-naming.FunctionIgnoredRegexp + value: '^BM_.*$' + - key: readability-identifier-naming.GlobalConstantCase + value: CamelCase + - key: readability-identifier-naming.GlobalConstantPrefix + value: k + - key: readability-identifier-naming.StaticConstantCase + value: CamelCase + - key: readability-identifier-naming.StaticConstantPrefix + value: k + - key: readability-identifier-naming.StaticVariableCase + value: lower_case + - key: readability-identifier-naming.MacroDefinitionCase + value: UPPER_CASE + - key: readability-identifier-naming.MacroDefinitionIgnoredRegexp + value: '^[A-Z]+(_[A-Z]+)*_$' + - key: readability-identifier-naming.MemberCase + value: lower_case + - key: readability-identifier-naming.PrivateMemberSuffix + value: _ + - key: readability-identifier-naming.PublicMemberSuffix + value: '' + - key: readability-identifier-naming.NamespaceCase + value: lower_case + - key: readability-identifier-naming.ParameterCase + value: lower_case + - key: readability-identifier-naming.TypeAliasCase + value: CamelCase + - key: readability-identifier-naming.TypedefCase + value: CamelCase + - key: readability-identifier-naming.VariableCase + value: lower_case + - key: readability-identifier-naming.IgnoreMainLikeFunctions + value: 'true' + - key: readability-identifier-naming.AggressiveDependentMemberLookup + value: 'false' + - key: readability-identifier-naming.IgnoreFailedSplit + value: 'false' + - key: abseil-string-find-startswith.AbseilStringsMatchHeader + value: 'absl/strings/match.h' + - key: abseil-string-find-startswith.IncludeStyle + value: llvm + - key: abseil-string-find-startswith.StringLikeClasses + value: '::std::basic_string' + - key: abseil-string-find-str-contains.AbseilStringsMatchHeader + value: 'absl/strings/match.h' + - key: abseil-string-find-str-contains.IncludeStyle + value: llvm + - key: abseil-string-find-str-contains.StringLikeClasses + value: '::std::basic_string;::std::basic_string_view;::absl::string_view' + - key: bugprone-argument-comment.CommentBoolLiterals + value: '0' + - key: bugprone-argument-comment.CommentCharacterLiterals + value: '0' + - key: bugprone-argument-comment.CommentFloatLiterals + value: '0' + - key: bugprone-argument-comment.CommentIntegerLiterals + value: '0' + - key: bugprone-argument-comment.CommentNullPtrs + value: '0' + - key: bugprone-argument-comment.CommentStringLiterals + value: '0' + - key: bugprone-argument-comment.CommentUserDefinedLiterals + value: '0' + - key: bugprone-argument-comment.IgnoreSingleArgument + value: '0' + - key: bugprone-argument-comment.StrictMode + value: '0' + - key: bugprone-assert-side-effect.AssertMacros + value: assert + - key: bugprone-assert-side-effect.CheckFunctionCalls + value: 'false' + - key: bugprone-dangling-handle.HandleClasses + value: 'std::basic_string_view;std::experimental::basic_string_view' + - key: bugprone-dynamic-static-initializers.HeaderFileExtensions + value: ';h;hh;hpp;hxx' + - key: bugprone-exception-escape.FunctionsThatShouldNotThrow + value: '' + - key: bugprone-exception-escape.IgnoredExceptions + value: '' + - key: bugprone-misplaced-widening-cast.CheckImplicitCasts + value: 'false' + - key: bugprone-narrowing-conversions.PedanticMode + value: 'false' + - key: bugprone-narrowing-conversions.WarnOnFloatingPointNarrowingConversion + value: 'true' + - key: bugprone-not-null-terminated-result.WantToUseSafeFunctions + value: 'true' + - key: bugprone-reserved-identifier.AggressiveDependentMemberLookup + value: 'false' + - key: bugprone-reserved-identifier.AllowedIdentifiers + value: '' + - key: bugprone-reserved-identifier.Invert + value: 'false' + - key: bugprone-signed-char-misuse.CharTypdefsToIgnore + value: '' + - key: bugprone-signed-char-misuse.DiagnoseSignedUnsignedCharComparisons + value: 'true' + - key: bugprone-sizeof-expression.WarnOnSizeOfCompareToConstant + value: 'true' + - key: bugprone-sizeof-expression.WarnOnSizeOfConstant + value: 'true' + - key: bugprone-sizeof-expression.WarnOnSizeOfIntegerExpression + value: 'false' + - key: bugprone-sizeof-expression.WarnOnSizeOfThis + value: 'true' + - key: bugprone-string-constructor.LargeLengthThreshold + value: '8388608' + - key: bugprone-string-constructor.WarnOnLargeLength + value: 'true' + - key: bugprone-suspicious-enum-usage.StrictMode + value: 'false' + - key: bugprone-suspicious-include.HeaderFileExtensions + value: ';h;hh;hpp;hxx' + - key: bugprone-suspicious-include.ImplementationFileExtensions + value: 'c;cc;cpp;cxx' + - key: bugprone-suspicious-missing-comma.MaxConcatenatedTokens + value: '5' + - key: bugprone-suspicious-missing-comma.RatioThreshold + value: '0.200000' + - key: bugprone-suspicious-missing-comma.SizeThreshold + value: '5' + - key: bugprone-suspicious-string-compare.StringCompareLikeFunctions + value: '' + - key: bugprone-suspicious-string-compare.WarnOnImplicitComparison + value: 'true' + - key: bugprone-suspicious-string-compare.WarnOnLogicalNotComparison + value: 'false' + - key: bugprone-too-small-loop-variable.MagnitudeBitsUpperLimit + value: '16' + - key: bugprone-unhandled-self-assignment.WarnOnlyIfThisHasSuspiciousField + value: 'true' + - key: bugprone-unused-return-value.CheckedFunctions + value: '::std::async;::std::launder;::std::remove;::std::remove_if;::std::unique;::std::unique_ptr::release;::std::basic_string::empty;::std::vector::empty;::std::back_inserter;::std::distance;::std::find;::std::find_if;::std::inserter;::std::lower_bound;::std::make_pair;::std::map::count;::std::map::find;::std::map::lower_bound;::std::multimap::equal_range;::std::multimap::upper_bound;::std::set::count;::std::set::find;::std::setfill;::std::setprecision;::std::setw;::std::upper_bound;::std::vector::at;::bsearch;::ferror;::feof;::isalnum;::isalpha;::isblank;::iscntrl;::isdigit;::isgraph;::islower;::isprint;::ispunct;::isspace;::isupper;::iswalnum;::iswprint;::iswspace;::isxdigit;::memchr;::memcmp;::strcmp;::strcoll;::strncmp;::strpbrk;::strrchr;::strspn;::strstr;::wcscmp;::access;::bind;::connect;::difftime;::dlsym;::fnmatch;::getaddrinfo;::getopt;::htonl;::htons;::iconv_open;::inet_addr;::isascii;::isatty;::mmap;::newlocale;::openat;::pathconf;::pthread_equal;::pthread_getspecific;::pthread_mutex_trylock;::readdir;::readlink;::recvmsg;::regexec;::scandir;::semget;::setjmp;::shm_open;::shmget;::sigismember;::strcasecmp;::strsignal;::ttyname' + - key: cert-dcl16-c.IgnoreMacros + value: 'true' + - key: cert-dcl16-c.NewSuffixes + value: 'L;LL;LU;LLU' + - key: cert-dcl37-c.AggressiveDependentMemberLookup + value: 'false' + - key: cert-dcl37-c.AllowedIdentifiers + value: '' + - key: cert-dcl37-c.Invert + value: 'false' + - key: cert-dcl51-cpp.AggressiveDependentMemberLookup + value: 'false' + - key: cert-dcl51-cpp.AllowedIdentifiers + value: '' + - key: cert-dcl51-cpp.Invert + value: 'false' + - key: cert-dcl59-cpp.HeaderFileExtensions + value: ';h;hh;hpp;hxx' + - key: cert-err09-cpp.CheckThrowTemporaries + value: 'true' + - key: cert-err09-cpp.MaxSize + value: '1000000' + - key: cert-err09-cpp.WarnOnLargeObjects + value: 'false' + - key: cert-err61-cpp.CheckThrowTemporaries + value: 'true' + - key: cert-err61-cpp.MaxSize + value: '1000000' + - key: cert-err61-cpp.WarnOnLargeObjects + value: 'false' + - key: cert-msc32-c.DisallowedSeedTypes + value: 'time_t,std::time_t' + - key: cert-msc51-cpp.DisallowedSeedTypes + value: 'time_t,std::time_t' + - key: cert-oop11-cpp.IncludeStyle + value: llvm + - key: cert-oop54-cpp.WarnOnlyIfThisHasSuspiciousField + value: 'false' + - key: cert-oop57-cpp.MemCmpNames + value: '' + - key: cert-oop57-cpp.MemCpyNames + value: '' + - key: cert-oop57-cpp.MemSetNames + value: '' + - key: cert-str34-c.CharTypdefsToIgnore + value: '' + - key: cert-str34-c.DiagnoseSignedUnsignedCharComparisons + value: 'false' + - key: cppcoreguidelines-avoid-magic-numbers.IgnoreAllFloatingPointValues + value: 'false' + - key: cppcoreguidelines-avoid-magic-numbers.IgnoreBitFieldsWidths + value: 'true' + - key: cppcoreguidelines-avoid-magic-numbers.IgnorePowersOf2IntegerValues + value: 'false' + - key: cppcoreguidelines-avoid-magic-numbers.IgnoredFloatingPointValues + value: '1.0;100.0;' + - key: cppcoreguidelines-avoid-magic-numbers.IgnoredIntegerValues + value: '1;2;3;4;' + - key: cppcoreguidelines-explicit-virtual-functions.AllowOverrideAndFinal + value: 'false' + - key: cppcoreguidelines-explicit-virtual-functions.FinalSpelling + value: final + - key: cppcoreguidelines-explicit-virtual-functions.IgnoreDestructors + value: 'true' + - key: cppcoreguidelines-explicit-virtual-functions.OverrideSpelling + value: override + - key: cppcoreguidelines-init-variables.IncludeStyle + value: llvm + - key: cppcoreguidelines-init-variables.MathHeader + value: math.h + - key: cppcoreguidelines-macro-usage.AllowedRegexp + value: '^DEBUG_*' + - key: cppcoreguidelines-macro-usage.CheckCapsOnly + value: 'false' + - key: cppcoreguidelines-macro-usage.IgnoreCommandLineMacros + value: 'true' + - key: cppcoreguidelines-narrowing-conversions.PedanticMode + value: 'false' + - key: cppcoreguidelines-narrowing-conversions.WarnOnFloatingPointNarrowingConversion + value: 'true' + - key: cppcoreguidelines-no-malloc.Allocations + value: '::malloc;::calloc' + - key: cppcoreguidelines-no-malloc.Deallocations + value: '::free' + - key: cppcoreguidelines-no-malloc.Reallocations + value: '::realloc' + - key: cppcoreguidelines-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic + value: 'true' + - key: cppcoreguidelines-non-private-member-variables-in-classes.IgnorePublicMemberVariables + value: 'false' + - key: cppcoreguidelines-owning-memory.LegacyResourceConsumers + value: '::free;::realloc;::freopen;::fclose' + - key: cppcoreguidelines-owning-memory.LegacyResourceProducers + value: '::malloc;::aligned_alloc;::realloc;::calloc;::fopen;::freopen;::tmpfile' + - key: cppcoreguidelines-pro-bounds-constant-array-index.GslHeader + value: '' + - key: cppcoreguidelines-pro-bounds-constant-array-index.IncludeStyle + value: llvm + - key: cppcoreguidelines-pro-type-member-init.IgnoreArrays + value: 'false' + - key: cppcoreguidelines-pro-type-member-init.UseAssignment + value: 'false' + - key: cppcoreguidelines-special-member-functions.AllowMissingMoveFunctions + value: 'false' + - key: cppcoreguidelines-special-member-functions.AllowMissingMoveFunctionsWhenCopyIsDeleted + value: 'false' + - key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor + value: 'false' + - key: google-build-namespaces.HeaderFileExtensions + value: ';h;hh;hpp;hxx' + - key: google-global-names-in-headers.HeaderFileExtensions + value: ';h;hh;hpp;hxx' + - key: google-readability-braces-around-statements.ShortStatementLines + value: '1' + - key: google-readability-function-size.BranchThreshold + value: '4294967295' + - key: google-readability-function-size.LineThreshold + value: '4294967295' + - key: google-readability-function-size.NestingThreshold + value: '4294967295' + - key: google-readability-function-size.ParameterThreshold + value: '4294967295' + - key: google-readability-function-size.StatementThreshold + value: '800' + - key: google-readability-function-size.VariableThreshold + value: '4294967295' + - key: google-readability-namespace-comments.ShortNamespaceLines + value: '10' + - key: google-readability-namespace-comments.SpacesBeforeComments + value: '2' + - key: google-runtime-int.SignedTypePrefix + value: int + - key: google-runtime-int.TypeSuffix + value: '' + - key: google-runtime-int.UnsignedTypePrefix + value: uint + - key: google-runtime-references.IncludedTypes + value: '' + - key: hicpp-braces-around-statements.ShortStatementLines + value: '0' + - key: hicpp-function-size.BranchThreshold + value: '4294967295' + - key: hicpp-function-size.LineThreshold + value: '4294967295' + - key: hicpp-function-size.NestingThreshold + value: '4294967295' + - key: hicpp-function-size.ParameterThreshold + value: '4294967295' + - key: hicpp-function-size.StatementThreshold + value: '800' + - key: hicpp-function-size.VariableThreshold + value: '4294967295' + - key: hicpp-member-init.IgnoreArrays + value: 'false' + - key: hicpp-member-init.UseAssignment + value: 'false' + - key: hicpp-move-const-arg.CheckTriviallyCopyableMove + value: 'true' + - key: hicpp-multiway-paths-covered.WarnOnMissingElse + value: 'false' + - key: hicpp-no-malloc.Allocations + value: '::malloc;::calloc' + - key: hicpp-no-malloc.Deallocations + value: '::free' + - key: hicpp-no-malloc.Reallocations + value: '::realloc' + - key: hicpp-signed-bitwise.IgnorePositiveIntegerLiterals + value: 'false' + - key: hicpp-special-member-functions.AllowMissingMoveFunctions + value: 'false' + - key: hicpp-special-member-functions.AllowMissingMoveFunctionsWhenCopyIsDeleted + value: 'false' + - key: hicpp-special-member-functions.AllowSoleDefaultDtor + value: 'false' + - key: hicpp-uppercase-literal-suffix.IgnoreMacros + value: 'true' + - key: hicpp-uppercase-literal-suffix.NewSuffixes + value: '' + - key: hicpp-use-auto.MinTypeNameLength + value: '5' + - key: hicpp-use-auto.RemoveStars + value: 'false' + - key: hicpp-use-emplace.ContainersWithPushBack + value: '::std::vector;::std::list;::std::deque' + - key: hicpp-use-emplace.IgnoreImplicitConstructors + value: 'false' + - key: hicpp-use-emplace.SmartPointers + value: '::std::shared_ptr;::std::unique_ptr;::std::auto_ptr;::std::weak_ptr' + - key: hicpp-use-emplace.TupleMakeFunctions + value: '::std::make_pair;::std::make_tuple' + - key: hicpp-use-emplace.TupleTypes + value: '::std::pair;::std::tuple' + - key: hicpp-use-equals-default.IgnoreMacros + value: 'true' + - key: hicpp-use-equals-delete.IgnoreMacros + value: 'true' + - key: hicpp-use-noexcept.ReplacementString + value: '' + - key: hicpp-use-noexcept.UseNoexceptFalse + value: 'true' + - key: hicpp-use-nullptr.NullMacros + value: '' + - key: hicpp-use-override.AllowOverrideAndFinal + value: 'false' + - key: hicpp-use-override.FinalSpelling + value: final + - key: hicpp-use-override.IgnoreDestructors + value: 'false' + - key: hicpp-use-override.OverrideSpelling + value: override + - key: llvm-else-after-return.WarnOnConditionVariables + value: 'false' + - key: llvm-else-after-return.WarnOnUnfixable + value: 'false' + - key: llvm-header-guard.HeaderFileExtensions + value: ';h;hh;hpp;hxx' + - key: llvm-namespace-comment.ShortNamespaceLines + value: '1' + - key: llvm-namespace-comment.SpacesBeforeComments + value: '1' + - key: llvm-qualified-auto.AddConstToQualified + value: 'false' + - key: misc-definitions-in-headers.HeaderFileExtensions + value: ';h;hh;hpp;hxx' + - key: misc-definitions-in-headers.UseHeaderFileExtension + value: 'true' + - key: misc-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic + value: 'false' + - key: misc-non-private-member-variables-in-classes.IgnorePublicMemberVariables + value: 'false' + - key: misc-throw-by-value-catch-by-reference.CheckThrowTemporaries + value: 'true' + - key: misc-throw-by-value-catch-by-reference.MaxSize + value: '1000000' + - key: misc-throw-by-value-catch-by-reference.WarnOnLargeObjects + value: 'false' + - key: misc-unused-parameters.StrictMode + value: 'false' + - key: modernize-avoid-bind.PermissiveParameterList + value: 'false' + - key: modernize-loop-convert.MaxCopySize + value: '16' + - key: modernize-loop-convert.MinConfidence + value: reasonable + - key: modernize-loop-convert.NamingStyle + value: CamelCase + - key: modernize-make-shared.IgnoreMacros + value: 'true' + - key: modernize-make-shared.IncludeStyle + value: llvm + - key: modernize-make-shared.MakeSmartPtrFunction + value: 'std::make_shared' + - key: modernize-make-shared.MakeSmartPtrFunctionHeader + value: memory + - key: modernize-make-unique.IgnoreMacros + value: 'true' + - key: modernize-make-unique.IncludeStyle + value: llvm + - key: modernize-make-unique.MakeSmartPtrFunction + value: 'std::make_unique' + - key: modernize-make-unique.MakeSmartPtrFunctionHeader + value: memory + - key: modernize-pass-by-value.IncludeStyle + value: llvm + - key: modernize-pass-by-value.ValuesOnly + value: 'false' + - key: modernize-raw-string-literal.DelimiterStem + value: lit + - key: modernize-raw-string-literal.ReplaceShorterLiterals + value: 'false' + - key: modernize-replace-auto-ptr.IncludeStyle + value: llvm + - key: modernize-replace-disallow-copy-and-assign-macro.MacroName + value: DISALLOW_COPY_AND_ASSIGN + - key: modernize-replace-random-shuffle.IncludeStyle + value: llvm + - key: modernize-use-auto.MinTypeNameLength + value: '5' + - key: modernize-use-auto.RemoveStars + value: 'false' + - key: modernize-use-bool-literals.IgnoreMacros + value: 'true' + - key: modernize-use-default-member-init.IgnoreMacros + value: 'true' + - key: modernize-use-default-member-init.UseAssignment + value: 'false' + - key: modernize-use-emplace.ContainersWithPushBack + value: '::std::vector;::std::list;::std::deque' + - key: modernize-use-emplace.IgnoreImplicitConstructors + value: 'false' + - key: modernize-use-emplace.SmartPointers + value: '::std::shared_ptr;::std::unique_ptr;::std::auto_ptr;::std::weak_ptr' + - key: modernize-use-emplace.TupleMakeFunctions + value: '::std::make_pair;::std::make_tuple' + - key: modernize-use-emplace.TupleTypes + value: '::std::pair;::std::tuple' + - key: modernize-use-equals-default.IgnoreMacros + value: 'true' + - key: modernize-use-equals-delete.IgnoreMacros + value: 'true' + - key: modernize-use-nodiscard.ReplacementString + value: '[[nodiscard]]' + - key: modernize-use-noexcept.ReplacementString + value: '' + - key: modernize-use-noexcept.UseNoexceptFalse + value: 'true' + - key: modernize-use-nullptr.NullMacros + value: 'NULL' + - key: modernize-use-override.AllowOverrideAndFinal + value: 'false' + - key: modernize-use-override.FinalSpelling + value: final + - key: modernize-use-override.IgnoreDestructors + value: 'false' + - key: modernize-use-override.OverrideSpelling + value: override + - key: modernize-use-transparent-functors.SafeMode + value: 'false' + - key: modernize-use-using.IgnoreMacros + value: 'true' + - key: objc-forbidden-subclassing.ForbiddenSuperClassNames + value: 'ABNewPersonViewController;ABPeoplePickerNavigationController;ABPersonViewController;ABUnknownPersonViewController;NSHashTable;NSMapTable;NSPointerArray;NSPointerFunctions;NSTimer;UIActionSheet;UIAlertView;UIImagePickerController;UITextInputMode;UIWebView' + - key: openmp-exception-escape.IgnoredExceptions + value: '' + - key: performance-faster-string-find.StringLikeClasses + value: '::std::basic_string;::std::basic_string_view' + - key: performance-for-range-copy.AllowedTypes + value: '' + - key: performance-for-range-copy.WarnOnAllAutoCopies + value: 'false' + - key: performance-inefficient-string-concatenation.StrictMode + value: 'false' + - key: performance-inefficient-vector-operation.EnableProto + value: 'false' + - key: performance-inefficient-vector-operation.VectorLikeClasses + value: '::std::vector' + - key: performance-move-const-arg.CheckTriviallyCopyableMove + value: 'true' + - key: performance-move-constructor-init.IncludeStyle + value: llvm + - key: performance-no-automatic-move.AllowedTypes + value: '' + - key: performance-type-promotion-in-math-fn.IncludeStyle + value: llvm + - key: performance-unnecessary-copy-initialization.AllowedTypes + value: '' + - key: performance-unnecessary-value-param.AllowedTypes + value: '' + - key: performance-unnecessary-value-param.IncludeStyle + value: llvm + - key: portability-restrict-system-includes.Includes + value: '*' + - key: portability-simd-intrinsics.Std + value: '' + - key: portability-simd-intrinsics.Suggest + value: '0' + - key: readability-braces-around-statements.ShortStatementLines + value: '0' + - key: readability-else-after-return.WarnOnConditionVariables + value: 'true' + - key: readability-else-after-return.WarnOnUnfixable + value: 'true' + - key: readability-function-size.BranchThreshold + value: '4294967295' + - key: readability-function-size.LineThreshold + value: '4294967295' + - key: readability-function-size.NestingThreshold + value: '4294967295' + - key: readability-function-size.ParameterThreshold + value: '4294967295' + - key: readability-function-size.StatementThreshold + value: '800' + - key: readability-function-size.VariableThreshold + value: '4294967295' + - key: readability-implicit-bool-conversion.AllowIntegerConditions + value: 'false' + - key: readability-implicit-bool-conversion.AllowPointerConditions + value: 'false' + - key: readability-inconsistent-declaration-parameter-name.IgnoreMacros + value: 'true' + - key: readability-inconsistent-declaration-parameter-name.Strict + value: 'false' + - key: readability-magic-numbers.IgnoreAllFloatingPointValues + value: 'false' + - key: readability-magic-numbers.IgnoreBitFieldsWidths + value: 'true' + - key: readability-magic-numbers.IgnorePowersOf2IntegerValues + value: 'false' + - key: readability-magic-numbers.IgnoredFloatingPointValues + value: '1.0;100.0;' + - key: readability-magic-numbers.IgnoredIntegerValues + value: '1;2;3;4;' + - key: readability-qualified-auto.AddConstToQualified + value: 'true' + - key: readability-redundant-declaration.IgnoreMacros + value: 'true' + - key: readability-redundant-member-init.IgnoreBaseInCopyConstructors + value: 'false' + - key: readability-redundant-smartptr-get.IgnoreMacros + value: 'true' + - key: readability-redundant-string-init.StringNames + value: '::std::basic_string' + - key: readability-simplify-boolean-expr.ChainedConditionalAssignment + value: 'false' + - key: readability-simplify-boolean-expr.ChainedConditionalReturn + value: 'false' + - key: readability-simplify-subscript-expr.Types + value: '::std::basic_string;::std::basic_string_view;::std::vector;::std::array' + - key: readability-static-accessed-through-instance.NameSpecifierNestingThreshold + value: '3' + - key: readability-uppercase-literal-suffix.IgnoreMacros + value: 'true' + - key: readability-uppercase-literal-suffix.NewSuffixes + value: '' + - key: zircon-temporary-objects.Names + value: '' +# allow x,y,z single letter names, to be used for coordinates +# allow fd (file descriptor) as it's a common POSIX convention + - key: readability-identifier-length.IgnoredVariableNames + value: '^(x|y|z|m0|m1|fd|_)$' + - key: readability-identifier-length.IgnoredParameterNames + value: '^(x|y|z|m0|m1|fd|_)$' +... diff --git a/.github/workflows/build-deno-dll.yml b/.github/workflows/build-deno-dll.yml new file mode 100644 index 0000000..fb84c77 --- /dev/null +++ b/.github/workflows/build-deno-dll.yml @@ -0,0 +1,43 @@ +name: Build Deno Shared Library (DLL) + +on: + push: + branches: [ main, master ] + pull_request: + +jobs: + build-deno-dll: + name: Build shared library for Deno (windows-x86_64) + runs-on: windows-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup CMake >= 3.30 + uses: jwlawson/actions-setup-cmake@v2 + with: + cmake-version: "3.31.x" + + - name: Configure CMake (Release) + run: | + cmake -S . -B build -G "Visual Studio 17 2022" -A x64 + + - name: Build shared library (Release) + run: | + cmake --build build --config Release + + - name: Collect DLL artifact for Deno + shell: pwsh + run: | + New-Item -ItemType Directory -Force -Path deno-lib/windows-x86_64 | Out-Null + Copy-Item -Force -Path build/**/Release/*.dll -Destination deno-lib/windows-x86_64/ + Get-ChildItem -Recurse deno-lib | Format-Table -AutoSize + + - name: Upload DLL artifact for Deno + uses: actions/upload-artifact@v4 + with: + name: cpp-bindings-windows-dll-windows-x86_64 + path: deno-lib/** + + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index e528296..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Build DLL - -on: - push: - branches: - - main - - workflow_dispatch: - -jobs: - build: - runs-on: windows-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Install MinGW - run: choco install mingw - - - name: Compile DLL - run: | - mkdir -p ./build - g++ -shared -o ./build/main.dll ./src/main.cpp - g++ -shared -o ./build/test.dll ./src/test.cpp - - - name: Upload DLL artifact - uses: actions/upload-artifact@v4 - with: - name: cpp_bindings_windows - path: build/**/*.dll diff --git a/.github/workflows/cpp-tests.yml b/.github/workflows/cpp-tests.yml new file mode 100644 index 0000000..88ae616 --- /dev/null +++ b/.github/workflows/cpp-tests.yml @@ -0,0 +1,34 @@ +name: C++ Tests + +on: + push: + branches: [ main, master ] + pull_request: + +jobs: + cpp-tests: + name: C++ tests (msvc) + runs-on: windows-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup CMake >= 3.30 + uses: jwlawson/actions-setup-cmake@v2 + with: + cmake-version: "3.31.x" + + - name: Configure CMake (Release) + run: | + cmake -S . -B build -G "Visual Studio 17 2022" -A x64 + + - name: Build (Release) + run: | + cmake --build build --config Release + + - name: Run C++ unit/integration tests + run: | + ctest --test-dir build -C Release --output-on-failure + + diff --git a/.github/workflows/deno-tests.yml b/.github/workflows/deno-tests.yml new file mode 100644 index 0000000..e634bdb --- /dev/null +++ b/.github/workflows/deno-tests.yml @@ -0,0 +1,44 @@ +name: Deno Integration Tests + +on: + push: + branches: [ main, master ] + pull_request: + +jobs: + deno-tests: + name: Deno integration tests (deno ${{ matrix.deno }}) + runs-on: windows-latest + + strategy: + matrix: + deno: ["2.6.0", "2.5.0"] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup CMake >= 3.30 + uses: jwlawson/actions-setup-cmake@v2 + with: + cmake-version: "3.31.x" + + - name: Setup Deno + uses: denoland/setup-deno@v2 + with: + deno-version: ${{ matrix.deno }} + + - name: Configure CMake (Release) + run: | + cmake -S . -B build -G "Visual Studio 17 2022" -A x64 + + - name: Build (Release) + run: | + cmake --build build --config Release + + - name: Run Deno integration tests + working-directory: integration_tests + run: | + deno task test + + diff --git a/.github/workflows/publish-jsr.yml b/.github/workflows/publish-jsr.yml new file mode 100644 index 0000000..59b6db2 --- /dev/null +++ b/.github/workflows/publish-jsr.yml @@ -0,0 +1,65 @@ +name: Publish to JSR (@serial/cpp-bindings-windows) + +on: + workflow_dispatch: + inputs: {} + + push: + branches: + - "*" + tags: + - "v*" + +permissions: + contents: read + id-token: write + +jobs: + publish-jsr: + runs-on: windows-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup CMake >= 3.30 + uses: jwlawson/actions-setup-cmake@v2 + with: + cmake-version: "3.31.x" + + - name: Setup Deno + uses: denoland/setup-deno@v2 + with: + deno-version: "2.x" + + - name: Configure CMake (Release) + run: | + cmake -S . -B build -G "Visual Studio 17 2022" -A x64 + + - name: Build shared library (Release) + run: | + cmake --build build --config Release + + - name: Embed binary as JSON/base64 for JSR + shell: pwsh + run: | + $dll = Get-ChildItem -Recurse -File build | Where-Object { $_.Name -eq "cpp_bindings_windows.dll" } | Select-Object -First 1 + if (-not $dll) { throw "Could not find cpp_bindings_windows.dll under build/" } + ./jsr/scripts/embed_binary.ps1 $dll.FullName ./jsr/package/bin windows-x86_64 + Copy-Item -Force LICENSE ./jsr/package/LICENSE + + - name: Publish package to JSR (real) + if: github.ref_type == 'tag' + working-directory: jsr/package + run: | + deno publish --allow-dirty + + - name: Publish package to JSR (dry-run) + if: github.ref_type != 'tag' + working-directory: jsr/package + run: | + deno publish --allow-dirty --dry-run + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..750db42 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Build directories +build/ +_build/ +out/ + +# CMake +CMakeCache.txt +CMakeFiles/ +cmake_install.cmake +*.cmake +!cmake/CPM.cmake +!cmake/*.cmake +compile_commands.json + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Cache +.cache/ +*.cache + +# Generated files +generated/ +*.dll +*.lib +*.exp +*.pdb + +# OS +.DS_Store +Thumbs.db + + diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..7333a96 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,162 @@ +cmake_minimum_required(VERSION 3.30) + +# Windows-only project +if(NOT WIN32) + message(FATAL_ERROR "cpp-bindings-windows can only be built on Windows.") +endif() + +# Export compile commands to root directory +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +include(cmake/CPM.cmake) + +CPMAddPackage( + NAME cmake_git_versioning + GITHUB_REPOSITORY Katze719/cmake-git-versioning + GIT_TAG v1.0.1 +) + +# Include cmake-git-versioning module +include(${cmake_git_versioning_SOURCE_DIR}/cmake/cmake-git-versioning.cmake) + +get_git_version_info() + +project( + cpp-bindings-windows + VERSION "${GIT_VERSION_MAJOR}.${GIT_VERSION_MINOR}.${GIT_VERSION_PATCH}" + DESCRIPTION "Windows implementation of cpp-core serial communication bindings" + LANGUAGES CXX +) + +# Generate JSR package metadata from the same git-derived version as the library. +# We generate into the source directory to match the Linux repo behavior. +configure_file( + "${CMAKE_SOURCE_DIR}/jsr/package/jsr.json.in" + "${CMAKE_SOURCE_DIR}/jsr/package/jsr.json" + @ONLY +) + +# Set C++ standard +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +# Enable C++23 module support +set(CMAKE_CXX_MODULE_STD 23) +set(CMAKE_CXX_MODULE_EXTENSIONS OFF) + +CPMAddPackage( + NAME cpp_core + GITHUB_REPOSITORY Serial-IO/cpp-core + GIT_TAG main + OPTIONS + "CMAKE_EXPORT_COMPILE_COMMANDS OFF" +) + +# Generate version information +generate_git_version( + OUTPUT_DIR ${CMAKE_BINARY_DIR}/generated + OUTPUT_FILE version.hpp +) + +CPMAddPackage( + NAME GTest + GITHUB_REPOSITORY google/googletest + GIT_TAG v1.14.0 + OPTIONS + "INSTALL_GTEST OFF" + "gtest_force_shared_crt ON" +) + +include(CTest) +enable_testing() + +file( + GLOB_RECURSE LIB_SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp" +) + +add_library(cpp_bindings_windows SHARED ${LIB_SOURCES}) + +set_target_properties( + cpp_bindings_windows + PROPERTIES + VERSION "${GIT_VERSION_MAJOR}.${GIT_VERSION_MINOR}.${GIT_VERSION_PATCH}" + SOVERSION "${GIT_VERSION_MAJOR}" + OUTPUT_NAME cpp_bindings_windows +) + +target_include_directories( + cpp_bindings_windows + PUBLIC + $ +) + +target_link_libraries( + cpp_bindings_windows + PUBLIC + cpp_core::cpp_core +) + +target_compile_features(cpp_bindings_windows PUBLIC cxx_std_23) + +# Collect all test source files +file( + GLOB_RECURSE TEST_SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/tests/*.cpp" +) + +if(TEST_SOURCES) + add_executable(cpp_bindings_windows_tests ${TEST_SOURCES}) + + target_include_directories( + cpp_bindings_windows_tests + PRIVATE + ${CMAKE_BINARY_DIR}/generated + ) + + target_link_libraries( + cpp_bindings_windows_tests + PRIVATE + cpp_bindings_windows + GTest::gtest + GTest::gtest_main + GTest::gmock + ) + + target_compile_features(cpp_bindings_windows_tests PRIVATE cxx_std_23) + + include(GoogleTest) + gtest_discover_tests(cpp_bindings_windows_tests) +endif() + +include(GNUInstallDirs) + +install( + TARGETS cpp_bindings_windows + EXPORT cpp_bindings_windowsTargets + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) + +# Copy generated version header +install( + FILES ${CMAKE_BINARY_DIR}/generated/version.hpp + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + OPTIONAL +) + +if(CMAKE_EXPORT_COMPILE_COMMANDS AND EXISTS "${CMAKE_BINARY_DIR}/compile_commands.json") + add_custom_target( + copy-compile-commands + ALL + ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_BINARY_DIR}/compile_commands.json + ${CMAKE_SOURCE_DIR}/compile_commands.json + DEPENDS ${CMAKE_BINARY_DIR}/compile_commands.json + COMMENT "Copying compile_commands.json to project root" + ) +endif() + + diff --git a/README.md b/README.md index 1736fd4..0bd5b40 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# cpp-windows-bindings \ No newline at end of file +# cpp-windows-bindings diff --git a/cmake/CPM.cmake b/cmake/CPM.cmake new file mode 100644 index 0000000..3636ee5 --- /dev/null +++ b/cmake/CPM.cmake @@ -0,0 +1,1363 @@ +# CPM.cmake - CMake's missing package manager +# =========================================== +# See https://github.com/cpm-cmake/CPM.cmake for usage and update instructions. +# +# MIT License +# ----------- +#[[ + Copyright (c) 2019-2023 Lars Melchior and contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +]] + +cmake_minimum_required(VERSION 3.14 FATAL_ERROR) + +# Initialize logging prefix +if(NOT CPM_INDENT) + set(CPM_INDENT + "CPM:" + CACHE INTERNAL "" + ) +endif() + +if(NOT COMMAND cpm_message) + function(cpm_message) + message(${ARGV}) + endfunction() +endif() + +if(DEFINED EXTRACTED_CPM_VERSION) + set(CURRENT_CPM_VERSION "${EXTRACTED_CPM_VERSION}${CPM_DEVELOPMENT}") +else() + set(CURRENT_CPM_VERSION 0.42.0) +endif() + +get_filename_component(CPM_CURRENT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}" REALPATH) +if(CPM_DIRECTORY) + if(NOT CPM_DIRECTORY STREQUAL CPM_CURRENT_DIRECTORY) + if(CPM_VERSION VERSION_LESS CURRENT_CPM_VERSION) + message( + AUTHOR_WARNING + "${CPM_INDENT} \ +A dependency is using a more recent CPM version (${CURRENT_CPM_VERSION}) than the current project (${CPM_VERSION}). \ +It is recommended to upgrade CPM to the most recent version. \ +See https://github.com/cpm-cmake/CPM.cmake for more information." + ) + endif() + if(${CMAKE_VERSION} VERSION_LESS "3.17.0") + include(FetchContent) + endif() + return() + endif() + + get_property( + CPM_INITIALIZED GLOBAL "" + PROPERTY CPM_INITIALIZED + SET + ) + if(CPM_INITIALIZED) + return() + endif() +endif() + +if(CURRENT_CPM_VERSION MATCHES "development-version") + message( + WARNING "${CPM_INDENT} Your project is using an unstable development version of CPM.cmake. \ +Please update to a recent release if possible. \ +See https://github.com/cpm-cmake/CPM.cmake for details." + ) +endif() + +set_property(GLOBAL PROPERTY CPM_INITIALIZED true) + +macro(cpm_set_policies) + # the policy allows us to change options without caching + cmake_policy(SET CMP0077 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) + + # the policy allows us to change set(CACHE) without caching + if(POLICY CMP0126) + cmake_policy(SET CMP0126 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0126 NEW) + endif() + + # The policy uses the download time for timestamp, instead of the timestamp in the archive. This + # allows for proper rebuilds when a projects url changes + if(POLICY CMP0135) + cmake_policy(SET CMP0135 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0135 NEW) + endif() + + # treat relative git repository paths as being relative to the parent project's remote + if(POLICY CMP0150) + cmake_policy(SET CMP0150 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0150 NEW) + endif() +endmacro() +cpm_set_policies() + +option(CPM_USE_LOCAL_PACKAGES "Always try to use `find_package` to get dependencies" + $ENV{CPM_USE_LOCAL_PACKAGES} +) +option(CPM_LOCAL_PACKAGES_ONLY "Only use `find_package` to get dependencies" + $ENV{CPM_LOCAL_PACKAGES_ONLY} +) +option(CPM_DOWNLOAD_ALL "Always download dependencies from source" $ENV{CPM_DOWNLOAD_ALL}) +option(CPM_DONT_UPDATE_MODULE_PATH "Don't update the module path to allow using find_package" + $ENV{CPM_DONT_UPDATE_MODULE_PATH} +) +option(CPM_DONT_CREATE_PACKAGE_LOCK "Don't create a package lock file in the binary path" + $ENV{CPM_DONT_CREATE_PACKAGE_LOCK} +) +option(CPM_INCLUDE_ALL_IN_PACKAGE_LOCK + "Add all packages added through CPM.cmake to the package lock" + $ENV{CPM_INCLUDE_ALL_IN_PACKAGE_LOCK} +) +option(CPM_USE_NAMED_CACHE_DIRECTORIES + "Use additional directory of package name in cache on the most nested level." + $ENV{CPM_USE_NAMED_CACHE_DIRECTORIES} +) + +set(CPM_VERSION + ${CURRENT_CPM_VERSION} + CACHE INTERNAL "" +) +set(CPM_DIRECTORY + ${CPM_CURRENT_DIRECTORY} + CACHE INTERNAL "" +) +set(CPM_FILE + ${CMAKE_CURRENT_LIST_FILE} + CACHE INTERNAL "" +) +set(CPM_PACKAGES + "" + CACHE INTERNAL "" +) +set(CPM_DRY_RUN + OFF + CACHE INTERNAL "Don't download or configure dependencies (for testing)" +) + +if(DEFINED ENV{CPM_SOURCE_CACHE}) + set(CPM_SOURCE_CACHE_DEFAULT $ENV{CPM_SOURCE_CACHE}) +else() + set(CPM_SOURCE_CACHE_DEFAULT OFF) +endif() + +set(CPM_SOURCE_CACHE + ${CPM_SOURCE_CACHE_DEFAULT} + CACHE PATH "Directory to download CPM dependencies" +) + +if(NOT CPM_DONT_UPDATE_MODULE_PATH AND NOT DEFINED CMAKE_FIND_PACKAGE_REDIRECTS_DIR) + set(CPM_MODULE_PATH + "${CMAKE_BINARY_DIR}/CPM_modules" + CACHE INTERNAL "" + ) + # remove old modules + file(REMOVE_RECURSE ${CPM_MODULE_PATH}) + file(MAKE_DIRECTORY ${CPM_MODULE_PATH}) + # locally added CPM modules should override global packages + set(CMAKE_MODULE_PATH "${CPM_MODULE_PATH};${CMAKE_MODULE_PATH}") +endif() + +if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) + set(CPM_PACKAGE_LOCK_FILE + "${CMAKE_BINARY_DIR}/cpm-package-lock.cmake" + CACHE INTERNAL "" + ) + file(WRITE ${CPM_PACKAGE_LOCK_FILE} + "# CPM Package Lock\n# This file should be committed to version control\n\n" + ) +endif() + +include(FetchContent) + +# Try to infer package name from git repository uri (path or url) +function(cpm_package_name_from_git_uri URI RESULT) + if("${URI}" MATCHES "([^/:]+)/?.git/?$") + set(${RESULT} + ${CMAKE_MATCH_1} + PARENT_SCOPE + ) + else() + unset(${RESULT} PARENT_SCOPE) + endif() +endfunction() + +# Find the shortest hash that can be used eg, if origin_hash is +# cccb77ae9609d2768ed80dd42cec54f77b1f1455 the following files will be checked, until one is found +# that is either empty (allowing us to assign origin_hash), or whose contents matches ${origin_hash} +# +# * .../cccb.hash +# * .../cccb77ae.hash +# * .../cccb77ae9609.hash +# * .../cccb77ae9609d276.hash +# * etc +# +# We will be able to use a shorter path with very high probability, but in the (rare) event that the +# first couple characters collide, we will check longer and longer substrings. +function(cpm_get_shortest_hash source_cache_dir origin_hash short_hash_output_var) + # for compatibility with caches populated by a previous version of CPM, check if a directory using + # the full hash already exists + if(EXISTS "${source_cache_dir}/${origin_hash}") + set(${short_hash_output_var} + "${origin_hash}" + PARENT_SCOPE + ) + return() + endif() + + foreach(len RANGE 4 40 4) + string(SUBSTRING "${origin_hash}" 0 ${len} short_hash) + set(hash_lock ${source_cache_dir}/${short_hash}.lock) + set(hash_fp ${source_cache_dir}/${short_hash}.hash) + # Take a lock, so we don't have a race condition with another instance of cmake. We will release + # this lock when we can, however, if there is an error, we want to ensure it gets released on + # it's own on exit from the function. + file(LOCK ${hash_lock} GUARD FUNCTION) + + # Load the contents of .../${short_hash}.hash + file(TOUCH ${hash_fp}) + file(READ ${hash_fp} hash_fp_contents) + + if(hash_fp_contents STREQUAL "") + # Write the origin hash + file(WRITE ${hash_fp} ${origin_hash}) + file(LOCK ${hash_lock} RELEASE) + break() + elseif(hash_fp_contents STREQUAL origin_hash) + file(LOCK ${hash_lock} RELEASE) + break() + else() + file(LOCK ${hash_lock} RELEASE) + endif() + endforeach() + set(${short_hash_output_var} + "${short_hash}" + PARENT_SCOPE + ) +endfunction() + +# Try to infer package name and version from a url +function(cpm_package_name_and_ver_from_url url outName outVer) + if(url MATCHES "[/\\?]([a-zA-Z0-9_\\.-]+)\\.(tar|tar\\.gz|tar\\.bz2|zip|ZIP)(\\?|/|$)") + # We matched an archive + set(filename "${CMAKE_MATCH_1}") + + if(filename MATCHES "([a-zA-Z0-9_\\.-]+)[_-]v?(([0-9]+\\.)*[0-9]+[a-zA-Z0-9]*)") + # We matched - (ie foo-1.2.3) + set(${outName} + "${CMAKE_MATCH_1}" + PARENT_SCOPE + ) + set(${outVer} + "${CMAKE_MATCH_2}" + PARENT_SCOPE + ) + elseif(filename MATCHES "(([0-9]+\\.)+[0-9]+[a-zA-Z0-9]*)") + # We couldn't find a name, but we found a version + # + # In many cases (which we don't handle here) the url would look something like + # `irrelevant/ACTUAL_PACKAGE_NAME/irrelevant/1.2.3.zip`. In such a case we can't possibly + # distinguish the package name from the irrelevant bits. Moreover if we try to match the + # package name from the filename, we'd get bogus at best. + unset(${outName} PARENT_SCOPE) + set(${outVer} + "${CMAKE_MATCH_1}" + PARENT_SCOPE + ) + else() + # Boldly assume that the file name is the package name. + # + # Yes, something like `irrelevant/ACTUAL_NAME/irrelevant/download.zip` will ruin our day, but + # such cases should be quite rare. No popular service does this... we think. + set(${outName} + "${filename}" + PARENT_SCOPE + ) + unset(${outVer} PARENT_SCOPE) + endif() + else() + # No ideas yet what to do with non-archives + unset(${outName} PARENT_SCOPE) + unset(${outVer} PARENT_SCOPE) + endif() +endfunction() + +function(cpm_find_package NAME VERSION) + string(REPLACE " " ";" EXTRA_ARGS "${ARGN}") + find_package(${NAME} ${VERSION} ${EXTRA_ARGS} QUIET) + if(${CPM_ARGS_NAME}_FOUND) + if(DEFINED ${CPM_ARGS_NAME}_VERSION) + set(VERSION ${${CPM_ARGS_NAME}_VERSION}) + endif() + cpm_message(STATUS "${CPM_INDENT} Using local package ${CPM_ARGS_NAME}@${VERSION}") + CPMRegisterPackage(${CPM_ARGS_NAME} "${VERSION}") + set(CPM_PACKAGE_FOUND + YES + PARENT_SCOPE + ) + else() + set(CPM_PACKAGE_FOUND + NO + PARENT_SCOPE + ) + endif() +endfunction() + +# Create a custom FindXXX.cmake module for a CPM package This prevents `find_package(NAME)` from +# finding the system library +function(cpm_create_module_file Name) + if(NOT CPM_DONT_UPDATE_MODULE_PATH) + if(DEFINED CMAKE_FIND_PACKAGE_REDIRECTS_DIR) + # Redirect find_package calls to the CPM package. This is what FetchContent does when you set + # OVERRIDE_FIND_PACKAGE. The CMAKE_FIND_PACKAGE_REDIRECTS_DIR works for find_package in CONFIG + # mode, unlike the Find${Name}.cmake fallback. CMAKE_FIND_PACKAGE_REDIRECTS_DIR is not defined + # in script mode, or in CMake < 3.24. + # https://cmake.org/cmake/help/latest/module/FetchContent.html#fetchcontent-find-package-integration-examples + string(TOLOWER ${Name} NameLower) + file(WRITE ${CMAKE_FIND_PACKAGE_REDIRECTS_DIR}/${NameLower}-config.cmake + "include(\"\${CMAKE_CURRENT_LIST_DIR}/${NameLower}-extra.cmake\" OPTIONAL)\n" + "include(\"\${CMAKE_CURRENT_LIST_DIR}/${Name}Extra.cmake\" OPTIONAL)\n" + ) + file(WRITE ${CMAKE_FIND_PACKAGE_REDIRECTS_DIR}/${NameLower}-config-version.cmake + "set(PACKAGE_VERSION_COMPATIBLE TRUE)\n" "set(PACKAGE_VERSION_EXACT TRUE)\n" + ) + else() + file(WRITE ${CPM_MODULE_PATH}/Find${Name}.cmake + "include(\"${CPM_FILE}\")\n${ARGN}\nset(${Name}_FOUND TRUE)" + ) + endif() + endif() +endfunction() + +# Find a package locally or fallback to CPMAddPackage +function(CPMFindPackage) + set(oneValueArgs NAME VERSION GIT_TAG FIND_PACKAGE_ARGUMENTS) + + cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "" ${ARGN}) + + if(NOT DEFINED CPM_ARGS_VERSION) + if(DEFINED CPM_ARGS_GIT_TAG) + cpm_get_version_from_git_tag("${CPM_ARGS_GIT_TAG}" CPM_ARGS_VERSION) + endif() + endif() + + set(downloadPackage ${CPM_DOWNLOAD_ALL}) + if(DEFINED CPM_DOWNLOAD_${CPM_ARGS_NAME}) + set(downloadPackage ${CPM_DOWNLOAD_${CPM_ARGS_NAME}}) + elseif(DEFINED ENV{CPM_DOWNLOAD_${CPM_ARGS_NAME}}) + set(downloadPackage $ENV{CPM_DOWNLOAD_${CPM_ARGS_NAME}}) + endif() + if(downloadPackage) + CPMAddPackage(${ARGN}) + cpm_export_variables(${CPM_ARGS_NAME}) + return() + endif() + + cpm_find_package(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}" ${CPM_ARGS_FIND_PACKAGE_ARGUMENTS}) + + if(NOT CPM_PACKAGE_FOUND) + CPMAddPackage(${ARGN}) + cpm_export_variables(${CPM_ARGS_NAME}) + endif() + +endfunction() + +# checks if a package has been added before +function(cpm_check_if_package_already_added CPM_ARGS_NAME CPM_ARGS_VERSION) + if("${CPM_ARGS_NAME}" IN_LIST CPM_PACKAGES) + CPMGetPackageVersion(${CPM_ARGS_NAME} CPM_PACKAGE_VERSION) + if("${CPM_PACKAGE_VERSION}" VERSION_LESS "${CPM_ARGS_VERSION}") + message( + WARNING + "${CPM_INDENT} Requires a newer version of ${CPM_ARGS_NAME} (${CPM_ARGS_VERSION}) than currently included (${CPM_PACKAGE_VERSION})." + ) + endif() + cpm_get_fetch_properties(${CPM_ARGS_NAME}) + set(${CPM_ARGS_NAME}_ADDED NO) + set(CPM_PACKAGE_ALREADY_ADDED + YES + PARENT_SCOPE + ) + cpm_export_variables(${CPM_ARGS_NAME}) + else() + set(CPM_PACKAGE_ALREADY_ADDED + NO + PARENT_SCOPE + ) + endif() +endfunction() + +# Parse the argument of CPMAddPackage in case a single one was provided and convert it to a list of +# arguments which can then be parsed idiomatically. For example gh:foo/bar@1.2.3 will be converted +# to: GITHUB_REPOSITORY;foo/bar;VERSION;1.2.3 +function(cpm_parse_add_package_single_arg arg outArgs) + # Look for a scheme + if("${arg}" MATCHES "^([a-zA-Z]+):(.+)$") + string(TOLOWER "${CMAKE_MATCH_1}" scheme) + set(uri "${CMAKE_MATCH_2}") + + # Check for CPM-specific schemes + if(scheme STREQUAL "gh") + set(out "GITHUB_REPOSITORY;${uri}") + set(packageType "git") + elseif(scheme STREQUAL "gl") + set(out "GITLAB_REPOSITORY;${uri}") + set(packageType "git") + elseif(scheme STREQUAL "bb") + set(out "BITBUCKET_REPOSITORY;${uri}") + set(packageType "git") + # A CPM-specific scheme was not found. Looks like this is a generic URL so try to determine + # type + elseif(arg MATCHES ".git/?(@|#|$)") + set(out "GIT_REPOSITORY;${arg}") + set(packageType "git") + else() + # Fall back to a URL + set(out "URL;${arg}") + set(packageType "archive") + + # We could also check for SVN since FetchContent supports it, but SVN is so rare these days. + # We just won't bother with the additional complexity it will induce in this function. SVN is + # done by multi-arg + endif() + else() + if(arg MATCHES ".git/?(@|#|$)") + set(out "GIT_REPOSITORY;${arg}") + set(packageType "git") + else() + # Give up + message(FATAL_ERROR "${CPM_INDENT} Can't determine package type of '${arg}'") + endif() + endif() + + # For all packages we interpret @... as version. Only replace the last occurrence. Thus URIs + # containing '@' can be used + string(REGEX REPLACE "@([^@]+)$" ";VERSION;\\1" out "${out}") + + # Parse the rest according to package type + if(packageType STREQUAL "git") + # For git repos we interpret #... as a tag or branch or commit hash + string(REGEX REPLACE "#([^#]+)$" ";GIT_TAG;\\1" out "${out}") + elseif(packageType STREQUAL "archive") + # For archives we interpret #... as a URL hash. + string(REGEX REPLACE "#([^#]+)$" ";URL_HASH;\\1" out "${out}") + # We don't try to parse the version if it's not provided explicitly. cpm_get_version_from_url + # should do this at a later point + else() + # We should never get here. This is an assertion and hitting it means there's a problem with the + # code above. A packageType was set, but not handled by this if-else. + message(FATAL_ERROR "${CPM_INDENT} Unsupported package type '${packageType}' of '${arg}'") + endif() + + set(${outArgs} + ${out} + PARENT_SCOPE + ) +endfunction() + +# Check that the working directory for a git repo is clean +function(cpm_check_git_working_dir_is_clean repoPath gitTag isClean) + + find_package(Git REQUIRED) + + if(NOT GIT_EXECUTABLE) + # No git executable, assume directory is clean + set(${isClean} + TRUE + PARENT_SCOPE + ) + return() + endif() + + # check for uncommitted changes + execute_process( + COMMAND ${GIT_EXECUTABLE} status --porcelain + RESULT_VARIABLE resultGitStatus + OUTPUT_VARIABLE repoStatus + OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET + WORKING_DIRECTORY ${repoPath} + ) + if(resultGitStatus) + # not supposed to happen, assume clean anyway + message(WARNING "${CPM_INDENT} Calling git status on folder ${repoPath} failed") + set(${isClean} + TRUE + PARENT_SCOPE + ) + return() + endif() + + if(NOT "${repoStatus}" STREQUAL "") + set(${isClean} + FALSE + PARENT_SCOPE + ) + return() + endif() + + # check for committed changes + execute_process( + COMMAND ${GIT_EXECUTABLE} diff -s --exit-code ${gitTag} + RESULT_VARIABLE resultGitDiff + OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_QUIET + WORKING_DIRECTORY ${repoPath} + ) + + if(${resultGitDiff} EQUAL 0) + set(${isClean} + TRUE + PARENT_SCOPE + ) + else() + set(${isClean} + FALSE + PARENT_SCOPE + ) + endif() + +endfunction() + +# Add PATCH_COMMAND to CPM_ARGS_UNPARSED_ARGUMENTS. This method consumes a list of files in ARGN +# then generates a `PATCH_COMMAND` appropriate for `ExternalProject_Add()`. This command is appended +# to the parent scope's `CPM_ARGS_UNPARSED_ARGUMENTS`. +function(cpm_add_patches) + # Return if no patch files are supplied. + if(NOT ARGN) + return() + endif() + + # Find the patch program. + find_program(PATCH_EXECUTABLE patch) + if(CMAKE_HOST_WIN32 AND NOT PATCH_EXECUTABLE) + # The Windows git executable is distributed with patch.exe. Find the path to the executable, if + # it exists, then search `../usr/bin` and `../../usr/bin` for patch.exe. + find_package(Git QUIET) + if(GIT_EXECUTABLE) + get_filename_component(extra_search_path ${GIT_EXECUTABLE} DIRECTORY) + get_filename_component(extra_search_path_1up ${extra_search_path} DIRECTORY) + get_filename_component(extra_search_path_2up ${extra_search_path_1up} DIRECTORY) + find_program( + PATCH_EXECUTABLE patch HINTS "${extra_search_path_1up}/usr/bin" + "${extra_search_path_2up}/usr/bin" + ) + endif() + endif() + if(NOT PATCH_EXECUTABLE) + message(FATAL_ERROR "Couldn't find `patch` executable to use with PATCHES keyword.") + endif() + + # Create a temporary + set(temp_list ${CPM_ARGS_UNPARSED_ARGUMENTS}) + + # Ensure each file exists (or error out) and add it to the list. + set(first_item True) + foreach(PATCH_FILE ${ARGN}) + # Make sure the patch file exists, if we can't find it, try again in the current directory. + if(NOT EXISTS "${PATCH_FILE}") + if(NOT EXISTS "${CMAKE_CURRENT_LIST_DIR}/${PATCH_FILE}") + message(FATAL_ERROR "Couldn't find patch file: '${PATCH_FILE}'") + endif() + set(PATCH_FILE "${CMAKE_CURRENT_LIST_DIR}/${PATCH_FILE}") + endif() + + # Convert to absolute path for use with patch file command. + get_filename_component(PATCH_FILE "${PATCH_FILE}" ABSOLUTE) + + # The first patch entry must be preceded by "PATCH_COMMAND" while the following items are + # preceded by "&&". + if(first_item) + set(first_item False) + list(APPEND temp_list "PATCH_COMMAND") + else() + list(APPEND temp_list "&&") + endif() + # Add the patch command to the list + list(APPEND temp_list "${PATCH_EXECUTABLE}" "-p1" "<" "${PATCH_FILE}") + endforeach() + + # Move temp out into parent scope. + set(CPM_ARGS_UNPARSED_ARGUMENTS + ${temp_list} + PARENT_SCOPE + ) + +endfunction() + +# method to overwrite internal FetchContent properties, to allow using CPM.cmake to overload +# FetchContent calls. As these are internal cmake properties, this method should be used carefully +# and may need modification in future CMake versions. Source: +# https://github.com/Kitware/CMake/blob/dc3d0b5a0a7d26d43d6cfeb511e224533b5d188f/Modules/FetchContent.cmake#L1152 +function(cpm_override_fetchcontent contentName) + cmake_parse_arguments(PARSE_ARGV 1 arg "" "SOURCE_DIR;BINARY_DIR" "") + if(NOT "${arg_UNPARSED_ARGUMENTS}" STREQUAL "") + message(FATAL_ERROR "${CPM_INDENT} Unsupported arguments: ${arg_UNPARSED_ARGUMENTS}") + endif() + + string(TOLOWER ${contentName} contentNameLower) + set(prefix "_FetchContent_${contentNameLower}") + + set(propertyName "${prefix}_sourceDir") + define_property( + GLOBAL + PROPERTY ${propertyName} + BRIEF_DOCS "Internal implementation detail of FetchContent_Populate()" + FULL_DOCS "Details used by FetchContent_Populate() for ${contentName}" + ) + set_property(GLOBAL PROPERTY ${propertyName} "${arg_SOURCE_DIR}") + + set(propertyName "${prefix}_binaryDir") + define_property( + GLOBAL + PROPERTY ${propertyName} + BRIEF_DOCS "Internal implementation detail of FetchContent_Populate()" + FULL_DOCS "Details used by FetchContent_Populate() for ${contentName}" + ) + set_property(GLOBAL PROPERTY ${propertyName} "${arg_BINARY_DIR}") + + set(propertyName "${prefix}_populated") + define_property( + GLOBAL + PROPERTY ${propertyName} + BRIEF_DOCS "Internal implementation detail of FetchContent_Populate()" + FULL_DOCS "Details used by FetchContent_Populate() for ${contentName}" + ) + set_property(GLOBAL PROPERTY ${propertyName} TRUE) +endfunction() + +# Download and add a package from source +function(CPMAddPackage) + cpm_set_policies() + + set(oneValueArgs + NAME + FORCE + VERSION + GIT_TAG + DOWNLOAD_ONLY + GITHUB_REPOSITORY + GITLAB_REPOSITORY + BITBUCKET_REPOSITORY + GIT_REPOSITORY + SOURCE_DIR + FIND_PACKAGE_ARGUMENTS + NO_CACHE + SYSTEM + GIT_SHALLOW + EXCLUDE_FROM_ALL + SOURCE_SUBDIR + CUSTOM_CACHE_KEY + ) + + set(multiValueArgs URL OPTIONS DOWNLOAD_COMMAND PATCHES) + + list(LENGTH ARGN argnLength) + + # Parse single shorthand argument + if(argnLength EQUAL 1) + cpm_parse_add_package_single_arg("${ARGN}" ARGN) + + # The shorthand syntax implies EXCLUDE_FROM_ALL and SYSTEM + set(ARGN "${ARGN};EXCLUDE_FROM_ALL;YES;SYSTEM;YES;") + + # Parse URI shorthand argument + elseif(argnLength GREATER 1 AND "${ARGV0}" STREQUAL "URI") + list(REMOVE_AT ARGN 0 1) # remove "URI gh:<...>@version#tag" + cpm_parse_add_package_single_arg("${ARGV1}" ARGV0) + + set(ARGN "${ARGV0};EXCLUDE_FROM_ALL;YES;SYSTEM;YES;${ARGN}") + endif() + + cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "${multiValueArgs}" "${ARGN}") + + # Set default values for arguments + if(NOT DEFINED CPM_ARGS_VERSION) + if(DEFINED CPM_ARGS_GIT_TAG) + cpm_get_version_from_git_tag("${CPM_ARGS_GIT_TAG}" CPM_ARGS_VERSION) + endif() + endif() + + if(CPM_ARGS_DOWNLOAD_ONLY) + set(DOWNLOAD_ONLY ${CPM_ARGS_DOWNLOAD_ONLY}) + else() + set(DOWNLOAD_ONLY NO) + endif() + + if(DEFINED CPM_ARGS_GITHUB_REPOSITORY) + set(CPM_ARGS_GIT_REPOSITORY "https://github.com/${CPM_ARGS_GITHUB_REPOSITORY}.git") + elseif(DEFINED CPM_ARGS_GITLAB_REPOSITORY) + set(CPM_ARGS_GIT_REPOSITORY "https://gitlab.com/${CPM_ARGS_GITLAB_REPOSITORY}.git") + elseif(DEFINED CPM_ARGS_BITBUCKET_REPOSITORY) + set(CPM_ARGS_GIT_REPOSITORY "https://bitbucket.org/${CPM_ARGS_BITBUCKET_REPOSITORY}.git") + endif() + + if(DEFINED CPM_ARGS_GIT_REPOSITORY) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_REPOSITORY ${CPM_ARGS_GIT_REPOSITORY}) + if(NOT DEFINED CPM_ARGS_GIT_TAG) + set(CPM_ARGS_GIT_TAG v${CPM_ARGS_VERSION}) + endif() + + # If a name wasn't provided, try to infer it from the git repo + if(NOT DEFINED CPM_ARGS_NAME) + cpm_package_name_from_git_uri(${CPM_ARGS_GIT_REPOSITORY} CPM_ARGS_NAME) + endif() + endif() + + set(CPM_SKIP_FETCH FALSE) + + if(DEFINED CPM_ARGS_GIT_TAG) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_TAG ${CPM_ARGS_GIT_TAG}) + # If GIT_SHALLOW is explicitly specified, honor the value. + if(DEFINED CPM_ARGS_GIT_SHALLOW) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_SHALLOW ${CPM_ARGS_GIT_SHALLOW}) + endif() + endif() + + if(DEFINED CPM_ARGS_URL) + # If a name or version aren't provided, try to infer them from the URL + list(GET CPM_ARGS_URL 0 firstUrl) + cpm_package_name_and_ver_from_url(${firstUrl} nameFromUrl verFromUrl) + # If we fail to obtain name and version from the first URL, we could try other URLs if any. + # However multiple URLs are expected to be quite rare, so for now we won't bother. + + # If the caller provided their own name and version, they trump the inferred ones. + if(NOT DEFINED CPM_ARGS_NAME) + set(CPM_ARGS_NAME ${nameFromUrl}) + endif() + if(NOT DEFINED CPM_ARGS_VERSION) + set(CPM_ARGS_VERSION ${verFromUrl}) + endif() + + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS URL "${CPM_ARGS_URL}") + endif() + + # Check for required arguments + + if(NOT DEFINED CPM_ARGS_NAME) + message( + FATAL_ERROR + "${CPM_INDENT} 'NAME' was not provided and couldn't be automatically inferred for package added with arguments: '${ARGN}'" + ) + endif() + + # Check if package has been added before + cpm_check_if_package_already_added(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}") + if(CPM_PACKAGE_ALREADY_ADDED) + cpm_export_variables(${CPM_ARGS_NAME}) + return() + endif() + + # Check for manual overrides + if(NOT CPM_ARGS_FORCE AND NOT "${CPM_${CPM_ARGS_NAME}_SOURCE}" STREQUAL "") + set(PACKAGE_SOURCE ${CPM_${CPM_ARGS_NAME}_SOURCE}) + set(CPM_${CPM_ARGS_NAME}_SOURCE "") + CPMAddPackage( + NAME "${CPM_ARGS_NAME}" + SOURCE_DIR "${PACKAGE_SOURCE}" + EXCLUDE_FROM_ALL "${CPM_ARGS_EXCLUDE_FROM_ALL}" + SYSTEM "${CPM_ARGS_SYSTEM}" + PATCHES "${CPM_ARGS_PATCHES}" + OPTIONS "${CPM_ARGS_OPTIONS}" + SOURCE_SUBDIR "${CPM_ARGS_SOURCE_SUBDIR}" + DOWNLOAD_ONLY "${DOWNLOAD_ONLY}" + FORCE True + ) + cpm_export_variables(${CPM_ARGS_NAME}) + return() + endif() + + # Check for available declaration + if(NOT CPM_ARGS_FORCE AND NOT "${CPM_DECLARATION_${CPM_ARGS_NAME}}" STREQUAL "") + set(declaration ${CPM_DECLARATION_${CPM_ARGS_NAME}}) + set(CPM_DECLARATION_${CPM_ARGS_NAME} "") + CPMAddPackage(${declaration}) + cpm_export_variables(${CPM_ARGS_NAME}) + # checking again to ensure version and option compatibility + cpm_check_if_package_already_added(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}") + return() + endif() + + if(NOT CPM_ARGS_FORCE) + if(CPM_USE_LOCAL_PACKAGES OR CPM_LOCAL_PACKAGES_ONLY) + cpm_find_package(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}" ${CPM_ARGS_FIND_PACKAGE_ARGUMENTS}) + + if(CPM_PACKAGE_FOUND) + cpm_export_variables(${CPM_ARGS_NAME}) + return() + endif() + + if(CPM_LOCAL_PACKAGES_ONLY) + message( + SEND_ERROR + "${CPM_INDENT} ${CPM_ARGS_NAME} not found via find_package(${CPM_ARGS_NAME} ${CPM_ARGS_VERSION})" + ) + endif() + endif() + endif() + + CPMRegisterPackage("${CPM_ARGS_NAME}" "${CPM_ARGS_VERSION}") + + if(DEFINED CPM_ARGS_GIT_TAG) + set(PACKAGE_INFO "${CPM_ARGS_GIT_TAG}") + elseif(DEFINED CPM_ARGS_SOURCE_DIR) + set(PACKAGE_INFO "${CPM_ARGS_SOURCE_DIR}") + else() + set(PACKAGE_INFO "${CPM_ARGS_VERSION}") + endif() + + if(DEFINED FETCHCONTENT_BASE_DIR) + # respect user's FETCHCONTENT_BASE_DIR if set + set(CPM_FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR}) + else() + set(CPM_FETCHCONTENT_BASE_DIR ${CMAKE_BINARY_DIR}/_deps) + endif() + + cpm_add_patches(${CPM_ARGS_PATCHES}) + + if(DEFINED CPM_ARGS_DOWNLOAD_COMMAND) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS DOWNLOAD_COMMAND ${CPM_ARGS_DOWNLOAD_COMMAND}) + elseif(DEFINED CPM_ARGS_SOURCE_DIR) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS SOURCE_DIR ${CPM_ARGS_SOURCE_DIR}) + if(NOT IS_ABSOLUTE ${CPM_ARGS_SOURCE_DIR}) + # Expand `CPM_ARGS_SOURCE_DIR` relative path. This is important because EXISTS doesn't work + # for relative paths. + get_filename_component( + source_directory ${CPM_ARGS_SOURCE_DIR} REALPATH BASE_DIR ${CMAKE_CURRENT_BINARY_DIR} + ) + else() + set(source_directory ${CPM_ARGS_SOURCE_DIR}) + endif() + if(NOT EXISTS ${source_directory}) + string(TOLOWER ${CPM_ARGS_NAME} lower_case_name) + # remove timestamps so CMake will re-download the dependency + file(REMOVE_RECURSE "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-subbuild") + endif() + elseif(CPM_SOURCE_CACHE AND NOT CPM_ARGS_NO_CACHE) + string(TOLOWER ${CPM_ARGS_NAME} lower_case_name) + set(origin_parameters ${CPM_ARGS_UNPARSED_ARGUMENTS}) + list(SORT origin_parameters) + if(CPM_ARGS_CUSTOM_CACHE_KEY) + # Application set a custom unique directory name + set(download_directory ${CPM_SOURCE_CACHE}/${lower_case_name}/${CPM_ARGS_CUSTOM_CACHE_KEY}) + elseif(CPM_USE_NAMED_CACHE_DIRECTORIES) + string(SHA1 origin_hash "${origin_parameters};NEW_CACHE_STRUCTURE_TAG") + cpm_get_shortest_hash( + "${CPM_SOURCE_CACHE}/${lower_case_name}" # source cache directory + "${origin_hash}" # Input hash + origin_hash # Computed hash + ) + set(download_directory ${CPM_SOURCE_CACHE}/${lower_case_name}/${origin_hash}/${CPM_ARGS_NAME}) + else() + string(SHA1 origin_hash "${origin_parameters}") + cpm_get_shortest_hash( + "${CPM_SOURCE_CACHE}/${lower_case_name}" # source cache directory + "${origin_hash}" # Input hash + origin_hash # Computed hash + ) + set(download_directory ${CPM_SOURCE_CACHE}/${lower_case_name}/${origin_hash}) + endif() + # Expand `download_directory` relative path. This is important because EXISTS doesn't work for + # relative paths. + get_filename_component(download_directory ${download_directory} ABSOLUTE) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS SOURCE_DIR ${download_directory}) + + if(CPM_SOURCE_CACHE) + file(LOCK ${download_directory}/../cmake.lock) + endif() + + if(EXISTS ${download_directory}) + if(CPM_SOURCE_CACHE) + file(LOCK ${download_directory}/../cmake.lock RELEASE) + endif() + + cpm_store_fetch_properties( + ${CPM_ARGS_NAME} "${download_directory}" + "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-build" + ) + cpm_get_fetch_properties("${CPM_ARGS_NAME}") + + if(DEFINED CPM_ARGS_GIT_TAG AND NOT (PATCH_COMMAND IN_LIST CPM_ARGS_UNPARSED_ARGUMENTS)) + # warn if cache has been changed since checkout + cpm_check_git_working_dir_is_clean(${download_directory} ${CPM_ARGS_GIT_TAG} IS_CLEAN) + if(NOT ${IS_CLEAN}) + message( + WARNING "${CPM_INDENT} Cache for ${CPM_ARGS_NAME} (${download_directory}) is dirty" + ) + endif() + endif() + + cpm_add_subdirectory( + "${CPM_ARGS_NAME}" + "${DOWNLOAD_ONLY}" + "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" + "${${CPM_ARGS_NAME}_BINARY_DIR}" + "${CPM_ARGS_EXCLUDE_FROM_ALL}" + "${CPM_ARGS_SYSTEM}" + "${CPM_ARGS_OPTIONS}" + ) + set(PACKAGE_INFO "${PACKAGE_INFO} at ${download_directory}") + + # As the source dir is already cached/populated, we override the call to FetchContent. + set(CPM_SKIP_FETCH TRUE) + cpm_override_fetchcontent( + "${lower_case_name}" SOURCE_DIR "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" + BINARY_DIR "${${CPM_ARGS_NAME}_BINARY_DIR}" + ) + + else() + # Enable shallow clone when GIT_TAG is not a commit hash. Our guess may not be accurate, but + # it should guarantee no commit hash get mis-detected. + if(NOT DEFINED CPM_ARGS_GIT_SHALLOW) + cpm_is_git_tag_commit_hash("${CPM_ARGS_GIT_TAG}" IS_HASH) + if(NOT ${IS_HASH}) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_SHALLOW TRUE) + endif() + endif() + + # remove timestamps so CMake will re-download the dependency + file(REMOVE_RECURSE ${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-subbuild) + set(PACKAGE_INFO "${PACKAGE_INFO} to ${download_directory}") + endif() + endif() + + if(NOT "${DOWNLOAD_ONLY}") + cpm_create_module_file(${CPM_ARGS_NAME} "CPMAddPackage(\"${ARGN}\")") + endif() + + if(CPM_PACKAGE_LOCK_ENABLED) + if((CPM_ARGS_VERSION AND NOT CPM_ARGS_SOURCE_DIR) OR CPM_INCLUDE_ALL_IN_PACKAGE_LOCK) + cpm_add_to_package_lock(${CPM_ARGS_NAME} "${ARGN}") + elseif(CPM_ARGS_SOURCE_DIR) + cpm_add_comment_to_package_lock(${CPM_ARGS_NAME} "local directory") + else() + cpm_add_comment_to_package_lock(${CPM_ARGS_NAME} "${ARGN}") + endif() + endif() + + cpm_message( + STATUS "${CPM_INDENT} Adding package ${CPM_ARGS_NAME}@${CPM_ARGS_VERSION} (${PACKAGE_INFO})" + ) + + if(NOT CPM_SKIP_FETCH) + # CMake 3.28 added EXCLUDE, SYSTEM (3.25), and SOURCE_SUBDIR (3.18) to FetchContent_Declare. + # Calling FetchContent_MakeAvailable will then internally forward these options to + # add_subdirectory. Up until these changes, we had to call FetchContent_Populate and + # add_subdirectory separately, which is no longer necessary and has been deprecated as of 3.30. + # A Bug in CMake prevents us to use the non-deprecated functions until 3.30.3. + set(fetchContentDeclareExtraArgs "") + if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.30.3") + if(${CPM_ARGS_EXCLUDE_FROM_ALL}) + list(APPEND fetchContentDeclareExtraArgs EXCLUDE_FROM_ALL) + endif() + if(${CPM_ARGS_SYSTEM}) + list(APPEND fetchContentDeclareExtraArgs SYSTEM) + endif() + if(DEFINED CPM_ARGS_SOURCE_SUBDIR) + list(APPEND fetchContentDeclareExtraArgs SOURCE_SUBDIR ${CPM_ARGS_SOURCE_SUBDIR}) + endif() + # For CMake version <3.28 OPTIONS are parsed in cpm_add_subdirectory + if(CPM_ARGS_OPTIONS AND NOT DOWNLOAD_ONLY) + foreach(OPTION ${CPM_ARGS_OPTIONS}) + cpm_parse_option("${OPTION}") + set(${OPTION_KEY} "${OPTION_VALUE}") + endforeach() + endif() + endif() + cpm_declare_fetch( + "${CPM_ARGS_NAME}" ${fetchContentDeclareExtraArgs} "${CPM_ARGS_UNPARSED_ARGUMENTS}" + ) + + cpm_fetch_package("${CPM_ARGS_NAME}" ${DOWNLOAD_ONLY} populated ${CPM_ARGS_UNPARSED_ARGUMENTS}) + if(CPM_SOURCE_CACHE AND download_directory) + file(LOCK ${download_directory}/../cmake.lock RELEASE) + endif() + if(${populated} AND ${CMAKE_VERSION} VERSION_LESS "3.30.3") + cpm_add_subdirectory( + "${CPM_ARGS_NAME}" + "${DOWNLOAD_ONLY}" + "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" + "${${CPM_ARGS_NAME}_BINARY_DIR}" + "${CPM_ARGS_EXCLUDE_FROM_ALL}" + "${CPM_ARGS_SYSTEM}" + "${CPM_ARGS_OPTIONS}" + ) + endif() + cpm_get_fetch_properties("${CPM_ARGS_NAME}") + endif() + + set(${CPM_ARGS_NAME}_ADDED YES) + cpm_export_variables("${CPM_ARGS_NAME}") +endfunction() + +# Fetch a previously declared package +macro(CPMGetPackage Name) + if(DEFINED "CPM_DECLARATION_${Name}") + CPMAddPackage(NAME ${Name}) + else() + message(SEND_ERROR "${CPM_INDENT} Cannot retrieve package ${Name}: no declaration available") + endif() +endmacro() + +# export variables available to the caller to the parent scope expects ${CPM_ARGS_NAME} to be set +macro(cpm_export_variables name) + set(${name}_SOURCE_DIR + "${${name}_SOURCE_DIR}" + PARENT_SCOPE + ) + set(${name}_BINARY_DIR + "${${name}_BINARY_DIR}" + PARENT_SCOPE + ) + set(${name}_ADDED + "${${name}_ADDED}" + PARENT_SCOPE + ) + set(CPM_LAST_PACKAGE_NAME + "${name}" + PARENT_SCOPE + ) +endmacro() + +# declares a package, so that any call to CPMAddPackage for the package name will use these +# arguments instead. Previous declarations will not be overridden. +macro(CPMDeclarePackage Name) + if(NOT DEFINED "CPM_DECLARATION_${Name}") + set("CPM_DECLARATION_${Name}" "${ARGN}") + endif() +endmacro() + +function(cpm_add_to_package_lock Name) + if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) + cpm_prettify_package_arguments(PRETTY_ARGN false ${ARGN}) + file(APPEND ${CPM_PACKAGE_LOCK_FILE} "# ${Name}\nCPMDeclarePackage(${Name}\n${PRETTY_ARGN})\n") + endif() +endfunction() + +function(cpm_add_comment_to_package_lock Name) + if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) + cpm_prettify_package_arguments(PRETTY_ARGN true ${ARGN}) + file(APPEND ${CPM_PACKAGE_LOCK_FILE} + "# ${Name} (unversioned)\n# CPMDeclarePackage(${Name}\n${PRETTY_ARGN}#)\n" + ) + endif() +endfunction() + +# includes the package lock file if it exists and creates a target `cpm-update-package-lock` to +# update it +macro(CPMUsePackageLock file) + if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) + get_filename_component(CPM_ABSOLUTE_PACKAGE_LOCK_PATH ${file} ABSOLUTE) + if(EXISTS ${CPM_ABSOLUTE_PACKAGE_LOCK_PATH}) + include(${CPM_ABSOLUTE_PACKAGE_LOCK_PATH}) + endif() + if(NOT TARGET cpm-update-package-lock) + add_custom_target( + cpm-update-package-lock COMMAND ${CMAKE_COMMAND} -E copy ${CPM_PACKAGE_LOCK_FILE} + ${CPM_ABSOLUTE_PACKAGE_LOCK_PATH} + ) + endif() + set(CPM_PACKAGE_LOCK_ENABLED true) + endif() +endmacro() + +# registers a package that has been added to CPM +function(CPMRegisterPackage PACKAGE VERSION) + list(APPEND CPM_PACKAGES ${PACKAGE}) + set(CPM_PACKAGES + ${CPM_PACKAGES} + CACHE INTERNAL "" + ) + set("CPM_PACKAGE_${PACKAGE}_VERSION" + ${VERSION} + CACHE INTERNAL "" + ) +endfunction() + +# retrieve the current version of the package to ${OUTPUT} +function(CPMGetPackageVersion PACKAGE OUTPUT) + set(${OUTPUT} + "${CPM_PACKAGE_${PACKAGE}_VERSION}" + PARENT_SCOPE + ) +endfunction() + +# declares a package in FetchContent_Declare +function(cpm_declare_fetch PACKAGE) + if(${CPM_DRY_RUN}) + cpm_message(STATUS "${CPM_INDENT} Package not declared (dry run)") + return() + endif() + + FetchContent_Declare(${PACKAGE} ${ARGN}) +endfunction() + +# returns properties for a package previously defined by cpm_declare_fetch +function(cpm_get_fetch_properties PACKAGE) + if(${CPM_DRY_RUN}) + return() + endif() + + set(${PACKAGE}_SOURCE_DIR + "${CPM_PACKAGE_${PACKAGE}_SOURCE_DIR}" + PARENT_SCOPE + ) + set(${PACKAGE}_BINARY_DIR + "${CPM_PACKAGE_${PACKAGE}_BINARY_DIR}" + PARENT_SCOPE + ) +endfunction() + +function(cpm_store_fetch_properties PACKAGE source_dir binary_dir) + if(${CPM_DRY_RUN}) + return() + endif() + + set(CPM_PACKAGE_${PACKAGE}_SOURCE_DIR + "${source_dir}" + CACHE INTERNAL "" + ) + set(CPM_PACKAGE_${PACKAGE}_BINARY_DIR + "${binary_dir}" + CACHE INTERNAL "" + ) +endfunction() + +# adds a package as a subdirectory if viable, according to provided options +function( + cpm_add_subdirectory + PACKAGE + DOWNLOAD_ONLY + SOURCE_DIR + BINARY_DIR + EXCLUDE + SYSTEM + OPTIONS +) + + if(NOT DOWNLOAD_ONLY AND EXISTS ${SOURCE_DIR}/CMakeLists.txt) + set(addSubdirectoryExtraArgs "") + if(EXCLUDE) + list(APPEND addSubdirectoryExtraArgs EXCLUDE_FROM_ALL) + endif() + if("${SYSTEM}" AND "${CMAKE_VERSION}" VERSION_GREATER_EQUAL "3.25") + # https://cmake.org/cmake/help/latest/prop_dir/SYSTEM.html#prop_dir:SYSTEM + list(APPEND addSubdirectoryExtraArgs SYSTEM) + endif() + if(OPTIONS) + foreach(OPTION ${OPTIONS}) + cpm_parse_option("${OPTION}") + set(${OPTION_KEY} "${OPTION_VALUE}") + endforeach() + endif() + set(CPM_OLD_INDENT "${CPM_INDENT}") + set(CPM_INDENT "${CPM_INDENT} ${PACKAGE}:") + add_subdirectory(${SOURCE_DIR} ${BINARY_DIR} ${addSubdirectoryExtraArgs}) + set(CPM_INDENT "${CPM_OLD_INDENT}") + endif() +endfunction() + +# downloads a previously declared package via FetchContent and exports the variables +# `${PACKAGE}_SOURCE_DIR` and `${PACKAGE}_BINARY_DIR` to the parent scope +function(cpm_fetch_package PACKAGE DOWNLOAD_ONLY populated) + set(${populated} + FALSE + PARENT_SCOPE + ) + if(${CPM_DRY_RUN}) + cpm_message(STATUS "${CPM_INDENT} Package ${PACKAGE} not fetched (dry run)") + return() + endif() + + FetchContent_GetProperties(${PACKAGE}) + + string(TOLOWER "${PACKAGE}" lower_case_name) + + if(NOT ${lower_case_name}_POPULATED) + if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.30.3") + if(DOWNLOAD_ONLY) + # MakeAvailable will call add_subdirectory internally which is not what we want when + # DOWNLOAD_ONLY is set. Populate will only download the dependency without adding it to the + # build + FetchContent_Populate( + ${PACKAGE} + SOURCE_DIR "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-src" + BINARY_DIR "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-build" + SUBBUILD_DIR "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-subbuild" + ${ARGN} + ) + else() + FetchContent_MakeAvailable(${PACKAGE}) + endif() + else() + FetchContent_Populate(${PACKAGE}) + endif() + set(${populated} + TRUE + PARENT_SCOPE + ) + endif() + + cpm_store_fetch_properties( + ${CPM_ARGS_NAME} ${${lower_case_name}_SOURCE_DIR} ${${lower_case_name}_BINARY_DIR} + ) + + set(${PACKAGE}_SOURCE_DIR + ${${lower_case_name}_SOURCE_DIR} + PARENT_SCOPE + ) + set(${PACKAGE}_BINARY_DIR + ${${lower_case_name}_BINARY_DIR} + PARENT_SCOPE + ) +endfunction() + +# splits a package option +function(cpm_parse_option OPTION) + string(REGEX MATCH "^[^ ]+" OPTION_KEY "${OPTION}") + string(LENGTH "${OPTION}" OPTION_LENGTH) + string(LENGTH "${OPTION_KEY}" OPTION_KEY_LENGTH) + if(OPTION_KEY_LENGTH STREQUAL OPTION_LENGTH) + # no value for key provided, assume user wants to set option to "ON" + set(OPTION_VALUE "ON") + else() + math(EXPR OPTION_KEY_LENGTH "${OPTION_KEY_LENGTH}+1") + string(SUBSTRING "${OPTION}" "${OPTION_KEY_LENGTH}" "-1" OPTION_VALUE) + endif() + set(OPTION_KEY + "${OPTION_KEY}" + PARENT_SCOPE + ) + set(OPTION_VALUE + "${OPTION_VALUE}" + PARENT_SCOPE + ) +endfunction() + +# guesses the package version from a git tag +function(cpm_get_version_from_git_tag GIT_TAG RESULT) + string(LENGTH ${GIT_TAG} length) + if(length EQUAL 40) + # GIT_TAG is probably a git hash + set(${RESULT} + 0 + PARENT_SCOPE + ) + else() + string(REGEX MATCH "v?([0123456789.]*).*" _ ${GIT_TAG}) + set(${RESULT} + ${CMAKE_MATCH_1} + PARENT_SCOPE + ) + endif() +endfunction() + +# guesses if the git tag is a commit hash or an actual tag or a branch name. +function(cpm_is_git_tag_commit_hash GIT_TAG RESULT) + string(LENGTH "${GIT_TAG}" length) + # full hash has 40 characters, and short hash has at least 7 characters. + if(length LESS 7 OR length GREATER 40) + set(${RESULT} + 0 + PARENT_SCOPE + ) + else() + if(${GIT_TAG} MATCHES "^[a-fA-F0-9]+$") + set(${RESULT} + 1 + PARENT_SCOPE + ) + else() + set(${RESULT} + 0 + PARENT_SCOPE + ) + endif() + endif() +endfunction() + +function(cpm_prettify_package_arguments OUT_VAR IS_IN_COMMENT) + set(oneValueArgs + NAME + FORCE + VERSION + GIT_TAG + DOWNLOAD_ONLY + GITHUB_REPOSITORY + GITLAB_REPOSITORY + BITBUCKET_REPOSITORY + GIT_REPOSITORY + SOURCE_DIR + FIND_PACKAGE_ARGUMENTS + NO_CACHE + SYSTEM + GIT_SHALLOW + EXCLUDE_FROM_ALL + SOURCE_SUBDIR + ) + set(multiValueArgs URL OPTIONS DOWNLOAD_COMMAND) + cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + foreach(oneArgName ${oneValueArgs}) + if(DEFINED CPM_ARGS_${oneArgName}) + if(${IS_IN_COMMENT}) + string(APPEND PRETTY_OUT_VAR "#") + endif() + if(${oneArgName} STREQUAL "SOURCE_DIR") + string(REPLACE ${CMAKE_SOURCE_DIR} "\${CMAKE_SOURCE_DIR}" CPM_ARGS_${oneArgName} + ${CPM_ARGS_${oneArgName}} + ) + endif() + string(APPEND PRETTY_OUT_VAR " ${oneArgName} ${CPM_ARGS_${oneArgName}}\n") + endif() + endforeach() + foreach(multiArgName ${multiValueArgs}) + if(DEFINED CPM_ARGS_${multiArgName}) + if(${IS_IN_COMMENT}) + string(APPEND PRETTY_OUT_VAR "#") + endif() + string(APPEND PRETTY_OUT_VAR " ${multiArgName}\n") + foreach(singleOption ${CPM_ARGS_${multiArgName}}) + if(${IS_IN_COMMENT}) + string(APPEND PRETTY_OUT_VAR "#") + endif() + string(APPEND PRETTY_OUT_VAR " \"${singleOption}\"\n") + endforeach() + endif() + endforeach() + + if(NOT "${CPM_ARGS_UNPARSED_ARGUMENTS}" STREQUAL "") + if(${IS_IN_COMMENT}) + string(APPEND PRETTY_OUT_VAR "#") + endif() + string(APPEND PRETTY_OUT_VAR " ") + foreach(CPM_ARGS_UNPARSED_ARGUMENT ${CPM_ARGS_UNPARSED_ARGUMENTS}) + string(APPEND PRETTY_OUT_VAR " ${CPM_ARGS_UNPARSED_ARGUMENT}") + endforeach() + string(APPEND PRETTY_OUT_VAR "\n") + endif() + + set(${OUT_VAR} + ${PRETTY_OUT_VAR} + PARENT_SCOPE + ) + +endfunction() diff --git a/integration_tests/deno.json b/integration_tests/deno.json new file mode 100644 index 0000000..c6278a4 --- /dev/null +++ b/integration_tests/deno.json @@ -0,0 +1,14 @@ +{ + "tasks": { + "test": "deno test --allow-ffi --allow-read --allow-env integration_test.ts" + }, + "imports": { + "@std/assert": "jsr:@std/assert@^1.0.16" + }, + "fmt": { + "lineWidth": 120, + "indentWidth": 4 + } +} + + diff --git a/integration_tests/deno.lock b/integration_tests/deno.lock new file mode 100644 index 0000000..36a2a1f --- /dev/null +++ b/integration_tests/deno.lock @@ -0,0 +1,25 @@ +{ + "version": "5", + "specifiers": { + "jsr:@std/assert@^1.0.16": "1.0.16", + "jsr:@std/internal@^1.0.12": "1.0.12" + }, + "jsr": { + "@std/assert@1.0.16": { + "integrity": "6a7272ed1eaa77defe76e5ff63ca705d9c495077e2d5fd0126d2b53fc5bd6532", + "dependencies": [ + "jsr:@std/internal" + ] + }, + "@std/internal@1.0.12": { + "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" + } + }, + "workspace": { + "dependencies": [ + "jsr:@std/assert@^1.0.16" + ] + } +} + + diff --git a/integration_tests/ffi_bindings.ts b/integration_tests/ffi_bindings.ts new file mode 100644 index 0000000..dc26431 --- /dev/null +++ b/integration_tests/ffi_bindings.ts @@ -0,0 +1,73 @@ +/** + * Minimal FFI bindings for the cpp-bindings-windows shared library + * used by the Deno integration tests. + */ + +export type LoadedLibrary = Deno.DynamicLibrary; +export type SerialLib = LoadedLibrary["symbols"]; + +const symbols = { + serialOpen: { + parameters: ["pointer", "i32", "i32", "i32", "i32", "pointer"] as const, + result: "i64" as const, + }, + serialClose: { + parameters: ["i64", "pointer"] as const, + result: "i32" as const, + }, + serialRead: { + parameters: ["i64", "pointer", "i32", "i32", "i32", "pointer"] as const, + result: "i32" as const, + }, + serialWrite: { + parameters: ["i64", "pointer", "i32", "i32", "i32", "pointer"] as const, + result: "i32" as const, + }, +}; + +/** + * Load the cpp-bindings-windows shared library + * @param libraryPath Path to the .dll file (defaults to build directory) + * @returns Object containing the symbols and a close method + */ +export async function loadSerialLib( + libraryPath?: string, +): Promise { + await Promise.resolve(); + + const possiblePaths = [ + libraryPath, + "../build/Release/cpp_bindings_windows.dll", + "../build/cpp_bindings_windows/Release/cpp_bindings_windows.dll", + "../build/**/Release/cpp_bindings_windows.dll", + "./cpp_bindings_windows.dll", + ].filter((p): p is string => p !== undefined); + + let lib: LoadedLibrary | null = null; + let lastError: Error | null = null; + + for (const path of possiblePaths) { + try { + // Note: Deno does not support globs; keep entries explicit. + if (path.includes("*")) continue; + const loaded = Deno.dlopen(path, symbols) as LoadedLibrary; + lib = loaded; + break; + } catch (error) { + lastError = error as Error; + continue; + } + } + + if (!lib) { + throw new Error( + `Failed to load cpp-bindings-windows library. Tried paths: ${ + possiblePaths.join(", ") + }. Last error: ${lastError?.message}`, + ); + } + + return lib; +} + + diff --git a/integration_tests/integration_test.ts b/integration_tests/integration_test.ts new file mode 100644 index 0000000..80dd669 --- /dev/null +++ b/integration_tests/integration_test.ts @@ -0,0 +1,43 @@ +/** + * Minimal Deno integration tests for cpp-bindings-windows: + * - verify that the shared library can be loaded + * - verify that it can be cleanly unloaded again + */ + +import { assertExists } from "@std/assert"; +import { type LoadedLibrary, loadSerialLib, type SerialLib } from "./ffi_bindings.ts"; + +let lib: SerialLib | null = null; +let loadedLib: LoadedLibrary | null = null; + +Deno.test({ + name: "Load cpp-bindings-windows library", + async fn() { + loadedLib = await loadSerialLib(); + assertExists(loadedLib, "Failed to load cpp-bindings-windows library"); + lib = loadedLib.symbols; + + assertExists(lib.serialOpen); + assertExists(lib.serialClose); + assertExists(lib.serialRead); + assertExists(lib.serialWrite); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "Unload cpp-bindings-windows library", + async fn() { + await Promise.resolve(); + if (!loadedLib) return; + + loadedLib.close(); + loadedLib = null; + lib = null; + }, + sanitizeResources: false, + sanitizeOps: false, +}); + + diff --git a/jsr/package/README.md b/jsr/package/README.md new file mode 100644 index 0000000..f91666f --- /dev/null +++ b/jsr/package/README.md @@ -0,0 +1,21 @@ +# C++ Bindings Windows + +This package ships the native Windows shared library payload as a **JSON/base64 +blob**. + +## Usage + +Import the JSON and write the `.dll` to disk (consumer project example): + +```ts +import blob from "@serial/cpp-bindings-windows/bin/x86_64" with { + type: "json", +}; + +const bytes = new TextEncoder().encode(atob(blob.data)); + +const tempFilePath = Deno.makeTempFileSync(); +Deno.writeFileSync(tempFilePath, bytes, { mode: 0o755 }); + +// Now you can open the binary using for example `Deno.dlopen` +``` diff --git a/jsr/package/jsr.json b/jsr/package/jsr.json new file mode 100644 index 0000000..0da89f9 --- /dev/null +++ b/jsr/package/jsr.json @@ -0,0 +1,19 @@ +{ + "name": "@serial/cpp-bindings-windows", + "version": "0.0.0", + "license": "LICENSE", + "description": "Windows shared-library bindings for Serial-IO/cpp-core (distributed via JSON/base64 because JSR has limited binary support).", + "exports": { + "./bin/x86_64": "./bin/x86_64.json" + }, + "publish": { + "include": [ + "README.md", + "LICENSE", + "jsr.json", + "bin/**" + ] + } +} + + diff --git a/jsr/package/jsr.json.in b/jsr/package/jsr.json.in new file mode 100644 index 0000000..43601f6 --- /dev/null +++ b/jsr/package/jsr.json.in @@ -0,0 +1,19 @@ +{ + "name": "@serial/cpp-bindings-windows", + "version": "@PROJECT_VERSION@", + "license": "LICENSE", + "description": "Windows shared-library bindings for Serial-IO/cpp-core (distributed via JSON/base64 because JSR has limited binary support).", + "exports": { + "./bin/x86_64": "./bin/x86_64.json" + }, + "publish": { + "include": [ + "README.md", + "LICENSE", + "jsr.json", + "bin/**" + ] + } +} + + diff --git a/jsr/scripts/embed_binary.ps1 b/jsr/scripts/embed_binary.ps1 new file mode 100644 index 0000000..a3401fb --- /dev/null +++ b/jsr/scripts/embed_binary.ps1 @@ -0,0 +1,40 @@ +param( + [Parameter(Mandatory = $true)][string]$BinaryPath, + [Parameter(Mandatory = $true)][string]$JsrBinPath, + [Parameter(Mandatory = $true)][string]$Target +) + +$ErrorActionPreference = "Stop" + +if (-not (Test-Path -LiteralPath $BinaryPath)) { + throw "Error: Binary path does not exist: $BinaryPath" +} + +$item = Get-Item -LiteralPath $BinaryPath +if ($item.PSIsContainer) { + throw "Error: Binary path is a directory: $BinaryPath" +} + +New-Item -ItemType Directory -Force -Path $JsrBinPath | Out-Null + +$filename = [System.IO.Path]::GetFileName($BinaryPath) + +# Copy dll into the package bin folder with a stable name +Copy-Item -Force -LiteralPath $BinaryPath -Destination (Join-Path $JsrBinPath "x86_64.dll") + +$bytes = [System.IO.File]::ReadAllBytes($BinaryPath) +$b64 = [System.Convert]::ToBase64String($bytes) + +$obj = @{ + target = $Target + filename = $filename + encoding = "base64" + data = $b64 +} + +$json = ($obj | ConvertTo-Json -Compress) +$outPath = Join-Path $JsrBinPath "x86_64.json" + +Set-Content -Path $outPath -Value $json -NoNewline -Encoding utf8NoBOM + + diff --git a/jsr/scripts/embed_binary.sh b/jsr/scripts/embed_binary.sh new file mode 100755 index 0000000..2f500dd --- /dev/null +++ b/jsr/scripts/embed_binary.sh @@ -0,0 +1,35 @@ +#!/bin/sh +set -eu + +if [ "$#" -ne 3 ]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +BINARY_PATH=$1 +JSR_BIN_PATH=$2 +TARGET=$3 + +if [ ! -f "$BINARY_PATH" ]; then + echo "Error: Binary path is not a file: $BINARY_PATH" >&2 + exit 1 +fi + +FILENAME=$(basename "$BINARY_PATH") + +mkdir -p "$JSR_BIN_PATH" + +cp "$BINARY_PATH" "$JSR_BIN_PATH/x86_64.so" + +BASE64_DATA=$(base64 "$BINARY_PATH" | tr -d '\n') + +jq -n \ + --arg target "$TARGET" \ + --arg filename "$FILENAME" \ + --arg data "$BASE64_DATA" \ + '{ + target: $target, + filename: $filename, + encoding: "base64", + data: $data + }' > "$JSR_BIN_PATH/x86_64.json" diff --git a/src/detail/win32_helpers.hpp b/src/detail/win32_helpers.hpp new file mode 100644 index 0000000..0d076cf --- /dev/null +++ b/src/detail/win32_helpers.hpp @@ -0,0 +1,150 @@ +#pragma once + +#include + +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include + +#include +#include + +namespace cpp_bindings_windows::detail +{ +class UniqueHandle +{ + public: + UniqueHandle() = default; + explicit UniqueHandle(HANDLE h) : handle_(h) + { + } + + UniqueHandle(const UniqueHandle &) = delete; + auto operator=(const UniqueHandle &) -> UniqueHandle & = delete; + + UniqueHandle(UniqueHandle &&other) noexcept : handle_(other.handle_) + { + other.handle_ = nullptr; + } + auto operator=(UniqueHandle &&other) noexcept -> UniqueHandle & + { + if (this != &other) + { + reset(other.release()); + } + return *this; + } + + ~UniqueHandle() + { + reset(nullptr); + } + + [[nodiscard]] auto get() const -> HANDLE + { + return handle_; + } + + [[nodiscard]] auto valid() const -> bool + { + return handle_ != nullptr && handle_ != INVALID_HANDLE_VALUE; + } + + auto reset(HANDLE new_handle) -> void + { + if (valid()) + { + CloseHandle(handle_); + } + handle_ = new_handle; + } + + [[nodiscard]] auto release() -> HANDLE + { + HANDLE out = handle_; + handle_ = nullptr; + return out; + } + + private: + HANDLE handle_ = nullptr; +}; + +template +inline auto invokeErrorCallback(Callback error_callback, cpp_core::StatusCodes code, const char *message) -> void +{ + if (error_callback != nullptr) + { + error_callback(static_cast(code), message); + } +} + +template +inline auto failMsg(Callback error_callback, cpp_core::StatusCodes code, const char *message) -> Ret +{ + invokeErrorCallback(error_callback, code, message); + return static_cast(code); +} + +inline auto win32ErrorToString(DWORD err) -> std::string +{ + LPSTR buffer = nullptr; + const DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS; + const DWORD lang_id = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT); + + const DWORD len = FormatMessageA(flags, nullptr, err, lang_id, reinterpret_cast(&buffer), 0, nullptr); + if (len == 0 || buffer == nullptr) + { + return "Unknown Win32 error"; + } + + std::string msg(buffer, len); + LocalFree(buffer); + + while (!msg.empty() && (msg.back() == '\r' || msg.back() == '\n')) + { + msg.pop_back(); + } + return msg; +} + +template +inline auto failWin32(Callback error_callback, cpp_core::StatusCodes code) -> Ret +{ + if (error_callback != nullptr) + { + const DWORD err = GetLastError(); + const std::string msg = win32ErrorToString(err); + error_callback(static_cast(code), msg.c_str()); + } + return static_cast(code); +} + +inline auto bytesWaiting(HANDLE handle, int *out_bytes) -> bool +{ + if (out_bytes == nullptr) + { + return false; + } + *out_bytes = 0; + + DWORD errors = 0; + COMSTAT stat = {}; + if (ClearCommError(handle, &errors, &stat) == 0) + { + return false; + } + + if (stat.cbInQue > static_cast(INT_MAX)) + { + *out_bytes = INT_MAX; + } + else + { + *out_bytes = static_cast(stat.cbInQue); + } + return true; +} + +} // namespace cpp_bindings_windows::detail diff --git a/src/main.cpp b/src/main.cpp deleted file mode 100644 index e7e4aeb..0000000 --- a/src/main.cpp +++ /dev/null @@ -1,182 +0,0 @@ -#include -#include -#include -#include - -class SerialPort { -private: - HANDLE hSerial; - OVERLAPPED readOverlap; - OVERLAPPED writeOverlap; - -public: - SerialPort() : hSerial(INVALID_HANDLE_VALUE) { - ZeroMemory(&readOverlap, sizeof(OVERLAPPED)); - ZeroMemory(&writeOverlap, sizeof(OVERLAPPED)); - readOverlap.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); - writeOverlap.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); - } - - ~SerialPort() { - if (hSerial != INVALID_HANDLE_VALUE) { - CloseHandle(hSerial); - } - if (readOverlap.hEvent) CloseHandle(readOverlap.hEvent); - if (writeOverlap.hEvent) CloseHandle(writeOverlap.hEvent); - } - - bool open(const char* portName, int baudRate) { - std::string port = "\\\\.\\"; - port += portName; - - hSerial = CreateFileA( - port.c_str(), - GENERIC_READ | GENERIC_WRITE, - 0, - NULL, - OPEN_EXISTING, - FILE_FLAG_OVERLAPPED, - NULL - ); - - if (hSerial == INVALID_HANDLE_VALUE) { - return false; - } - - DCB dcbSerialParams = { 0 }; - dcbSerialParams.DCBlength = sizeof(dcbSerialParams); - - if (!GetCommState(hSerial, &dcbSerialParams)) { - CloseHandle(hSerial); - hSerial = INVALID_HANDLE_VALUE; - return false; - } - - dcbSerialParams.BaudRate = baudRate; - dcbSerialParams.ByteSize = 8; - dcbSerialParams.StopBits = ONESTOPBIT; - dcbSerialParams.Parity = NOPARITY; - - if (!SetCommState(hSerial, &dcbSerialParams)) { - CloseHandle(hSerial); - hSerial = INVALID_HANDLE_VALUE; - return false; - } - - COMMTIMEOUTS timeouts = { 0 }; - timeouts.ReadIntervalTimeout = MAXDWORD; - timeouts.ReadTotalTimeoutMultiplier = 0; - timeouts.ReadTotalTimeoutConstant = 0; - timeouts.WriteTotalTimeoutMultiplier = 0; - timeouts.WriteTotalTimeoutConstant = 0; - - if (!SetCommTimeouts(hSerial, &timeouts)) { - CloseHandle(hSerial); - hSerial = INVALID_HANDLE_VALUE; - return false; - } - - return true; - } - - bool close() { - if (hSerial != INVALID_HANDLE_VALUE) { - CloseHandle(hSerial); - hSerial = INVALID_HANDLE_VALUE; - return true; - } - return false; - } - - int readAsync(void* buffer, int bufferSize) { - if (hSerial == INVALID_HANDLE_VALUE) { - return 0; - } - - DWORD bytesRead = 0; - ResetEvent(readOverlap.hEvent); - - if (!ReadFile(hSerial, buffer, bufferSize, &bytesRead, &readOverlap)) { - if (GetLastError() == ERROR_IO_PENDING) { - if (WaitForSingleObject(readOverlap.hEvent, INFINITE) == WAIT_OBJECT_0) { - GetOverlappedResult(hSerial, &readOverlap, &bytesRead, FALSE); - } - } - } - return bytesRead; - } - - int writeAsync(void* buffer, int bufferSize) { - if (hSerial == INVALID_HANDLE_VALUE) { - return 0; - } - - DWORD bytesWritten = 0; - ResetEvent(writeOverlap.hEvent); - - if (!WriteFile(hSerial, buffer, bufferSize, &bytesWritten, &writeOverlap)) { - if (GetLastError() == ERROR_IO_PENDING) { - if (WaitForSingleObject(writeOverlap.hEvent, INFINITE) == WAIT_OBJECT_0) { - GetOverlappedResult(hSerial, &writeOverlap, &bytesWritten, FALSE); - } - } - } - return bytesWritten; - } -}; - -static SerialPort* port = nullptr; - -extern "C" { - __declspec(dllexport) int OpenSerialPort( - const uint8_t* portName, - int baudRate - ) { - return 0; - } - - __declspec(dllexport) int CloseSerialPort() { - try { - if (port != nullptr) { - bool result = port->close(); - delete port; - port = nullptr; - return result ? 1 : 0; - } - return 0; - } - catch (...) { - return 0; - } - } - - __declspec(dllexport) int ReadSerialPort( - void* buffer, - int bufferSize - ) { - try { - if (port == nullptr) { - return 0; - } - return port->readAsync(buffer, bufferSize); - } - catch (...) { - return 0; - } - } - - __declspec(dllexport) int WriteSerialPort( - void* buffer, - int bufferSize - ) { - try { - if (port == nullptr) { - return 0; - } - return port->writeAsync(buffer, bufferSize); - } - catch (...) { - return 0; - } - } -} diff --git a/src/serial_close.cpp b/src/serial_close.cpp new file mode 100644 index 0000000..748cbf4 --- /dev/null +++ b/src/serial_close.cpp @@ -0,0 +1,39 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +#include + +extern "C" +{ + + MODULE_API auto serialClose(int64_t handle, ErrorCallbackT error_callback) -> int + { + if (handle <= 0) + { + return static_cast(cpp_core::StatusCodes::kSuccess); + } + + if (handle > std::numeric_limits::max()) + { + return cpp_bindings_windows::detail::failMsg( + error_callback, cpp_core::StatusCodes::kInvalidHandleError, "Invalid handle"); + } + + const HANDLE h = reinterpret_cast(static_cast(handle)); + if (h == nullptr || h == INVALID_HANDLE_VALUE) + { + return static_cast(cpp_core::StatusCodes::kSuccess); + } + + if (CloseHandle(h) == 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCodes::kCloseHandleError); + } + + return static_cast(cpp_core::StatusCodes::kSuccess); + } + +} // extern "C" diff --git a/src/serial_open.cpp b/src/serial_open.cpp new file mode 100644 index 0000000..4e9d1e4 --- /dev/null +++ b/src/serial_open.cpp @@ -0,0 +1,164 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +#include + +namespace +{ +auto normalizePortPath(const wchar_t *port) -> std::wstring +{ + // Accept: + // - "COM3" + // - "\\\\.\\COM10" + // Always produce a path that works for COM1..COM*. + std::wstring p(port); + if (p.rfind(L"\\\\.\\", 0) == 0) + { + return p; + } + if (p.rfind(L"COM", 0) == 0 || p.rfind(L"com", 0) == 0) + { + return L"\\\\.\\" + p; + } + return p; +} + +auto applyLineSettings(HANDLE handle, int baudrate, int data_bits, int parity, int stop_bits) -> bool +{ + DCB dcb = {}; + dcb.DCBlength = sizeof(DCB); + + if (GetCommState(handle, &dcb) == 0) + { + return false; + } + + dcb.BaudRate = static_cast(baudrate); + dcb.ByteSize = static_cast(data_bits); + + dcb.fBinary = TRUE; + dcb.fParity = (parity != 0) ? TRUE : FALSE; + + // Disable flow control by default (matches the Linux impl's raw mode + // behavior) + dcb.fOutxCtsFlow = FALSE; + dcb.fOutxDsrFlow = FALSE; + dcb.fDtrControl = DTR_CONTROL_ENABLE; + dcb.fDsrSensitivity = FALSE; + dcb.fTXContinueOnXoff = TRUE; + dcb.fOutX = FALSE; + dcb.fInX = FALSE; + dcb.fRtsControl = RTS_CONTROL_ENABLE; + + switch (parity) + { + case 0: + dcb.Parity = NOPARITY; + break; + case 1: + dcb.Parity = EVENPARITY; + break; + case 2: + dcb.Parity = ODDPARITY; + break; + default: + SetLastError(ERROR_INVALID_PARAMETER); + return false; + } + + // stop_bits mapping: + // 0 or 1 = 1 stop bit (0 kept for backward compatibility with callers using + // "default") 2 = 2 stop bits + if (stop_bits == 0 || stop_bits == 1) + { + dcb.StopBits = ONESTOPBIT; + } + else if (stop_bits == 2) + { + dcb.StopBits = TWOSTOPBITS; + } + else + { + SetLastError(ERROR_INVALID_PARAMETER); + return false; + } + + if (SetCommState(handle, &dcb) == 0) + { + return false; + } + + // Use overlapped operations + explicit waits in read/write, so keep + // COMMTIMEOUTS as 0. + COMMTIMEOUTS timeouts = {}; + if (SetCommTimeouts(handle, &timeouts) == 0) + { + return false; + } + + return true; +} +} // namespace + +extern "C" +{ + MODULE_API auto serialOpen(void *port, int baudrate, int data_bits, int parity, int stop_bits, + ErrorCallbackT error_callback) -> intptr_t + { + if (port == nullptr) + { + return cpp_bindings_windows::detail::failMsg( + error_callback, cpp_core::StatusCodes::kNotFoundError, "Port parameter is nullptr"); + } + + if (baudrate < 300) + { + return cpp_bindings_windows::detail::failMsg( + error_callback, cpp_core::StatusCodes::kSetStateError, "Invalid baudrate: must be >= 300"); + } + + if (data_bits < 5 || data_bits > 8) + { + return cpp_bindings_windows::detail::failMsg( + error_callback, cpp_core::StatusCodes::kSetStateError, "Invalid data bits: must be 5-8"); + } + + // Windows: port is documented as const wchar_t* + const auto *port_path = static_cast(port); + const std::wstring device_path = normalizePortPath(port_path); + + cpp_bindings_windows::detail::UniqueHandle handle( + CreateFileW(device_path.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, nullptr)); + + if (!handle.valid()) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCodes::kNotFoundError); + } + + if (!applyLineSettings(handle.get(), baudrate, data_bits, parity, stop_bits)) + { + const cpp_core::StatusCodes code = (GetLastError() == ERROR_INVALID_PARAMETER) + ? cpp_core::StatusCodes::kSetStateError + : cpp_core::StatusCodes::kSetStateError; + return cpp_bindings_windows::detail::failWin32(error_callback, code); + } + + // Clear buffers (best-effort) + PurgeComm(handle.get(), PURGE_RXCLEAR | PURGE_TXCLEAR | PURGE_RXABORT | PURGE_TXABORT); + + const intptr_t out = reinterpret_cast(handle.release()); + if (out <= 0) + { + // Extremely unlikely, but keep the API contract: success -> positive + // handle. + return cpp_bindings_windows::detail::failMsg( + error_callback, cpp_core::StatusCodes::kInvalidHandleError, "Invalid handle generated"); + } + return out; + } + +} // extern "C" diff --git a/src/serial_read.cpp b/src/serial_read.cpp new file mode 100644 index 0000000..0f438f5 --- /dev/null +++ b/src/serial_read.cpp @@ -0,0 +1,238 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +#include +#include + +namespace +{ +auto waitForRxChar(HANDLE handle, int timeout_ms) -> int +{ + // Returns: -1 on error, 0 on timeout, 1 on data available. + if (timeout_ms < 0) + { + timeout_ms = 0; + } + + if (SetCommMask(handle, EV_RXCHAR) == 0) + { + return -1; + } + + OVERLAPPED ov = {}; + ov.hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (ov.hEvent == nullptr) + { + return -1; + } + + DWORD mask = 0; + const BOOL ok = WaitCommEvent(handle, &mask, &ov); + if (ok == 0) + { + const DWORD err = GetLastError(); + if (err != ERROR_IO_PENDING) + { + CloseHandle(ov.hEvent); + SetLastError(err); + return -1; + } + } + + const DWORD wait_rc = WaitForSingleObject(ov.hEvent, static_cast(timeout_ms)); + if (wait_rc == WAIT_TIMEOUT) + { + CancelIoEx(handle, &ov); + CloseHandle(ov.hEvent); + return 0; + } + if (wait_rc != WAIT_OBJECT_0) + { + CancelIoEx(handle, &ov); + CloseHandle(ov.hEvent); + SetLastError(ERROR_GEN_FAILURE); + return -1; + } + + DWORD bytes = 0; + if (GetOverlappedResult(handle, &ov, &bytes, FALSE) == 0) + { + const DWORD err = GetLastError(); + CloseHandle(ov.hEvent); + SetLastError(err); + return -1; + } + + CloseHandle(ov.hEvent); + return 1; +} + +auto readSome(HANDLE handle, unsigned char *dst, int size, int timeout_ms) -> int +{ + if (size <= 0) + { + return 0; + } + + OVERLAPPED ov = {}; + ov.hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (ov.hEvent == nullptr) + { + SetLastError(ERROR_NOT_ENOUGH_MEMORY); + return -1; + } + + DWORD bytes_read = 0; + const BOOL ok = ReadFile(handle, dst, static_cast(size), &bytes_read, &ov); + if (ok != 0) + { + CloseHandle(ov.hEvent); + return static_cast(bytes_read); + } + + const DWORD err = GetLastError(); + if (err != ERROR_IO_PENDING) + { + CloseHandle(ov.hEvent); + SetLastError(err); + return -1; + } + + if (timeout_ms < 0) + { + timeout_ms = 0; + } + const DWORD wait_rc = WaitForSingleObject(ov.hEvent, static_cast(timeout_ms)); + if (wait_rc == WAIT_TIMEOUT) + { + CancelIoEx(handle, &ov); + CloseHandle(ov.hEvent); + return 0; + } + if (wait_rc != WAIT_OBJECT_0) + { + CancelIoEx(handle, &ov); + CloseHandle(ov.hEvent); + SetLastError(ERROR_GEN_FAILURE); + return -1; + } + + if (GetOverlappedResult(handle, &ov, &bytes_read, FALSE) == 0) + { + const DWORD err2 = GetLastError(); + CloseHandle(ov.hEvent); + SetLastError(err2); + return -1; + } + + CloseHandle(ov.hEvent); + return static_cast(bytes_read); +} +} // namespace + +extern "C" +{ + MODULE_API auto serialRead(int64_t handle, void *buffer, int buffer_size, int timeout_ms, int /*multiplier*/, + ErrorCallbackT error_callback) -> int + { + if (buffer == nullptr || buffer_size <= 0) + { + return cpp_bindings_windows::detail::failMsg(error_callback, cpp_core::StatusCodes::kBufferError, + "Invalid buffer or buffer_size"); + } + + if (handle <= 0 || handle > std::numeric_limits::max()) + { + return cpp_bindings_windows::detail::failMsg( + error_callback, cpp_core::StatusCodes::kInvalidHandleError, "Invalid handle"); + } + + const HANDLE h = reinterpret_cast(static_cast(handle)); + if (h == nullptr || h == INVALID_HANDLE_VALUE) + { + return cpp_bindings_windows::detail::failMsg( + error_callback, cpp_core::StatusCodes::kInvalidHandleError, "Invalid handle"); + } + + auto *buf = static_cast(buffer); + + int waiting = 0; + if (!cpp_bindings_windows::detail::bytesWaiting(h, &waiting)) + { + return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kGetStateError); + } + + if (waiting <= 0) + { + if (timeout_ms <= 0) + { + return 0; + } + const int ready = waitForRxChar(h, timeout_ms); + if (ready < 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kReadError); + } + if (ready == 0) + { + return 0; + } + } + + // Re-check queue after waiting + if (!cpp_bindings_windows::detail::bytesWaiting(h, &waiting)) + { + return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kGetStateError); + } + + if (waiting <= 0) + { + return 0; + } + + const int first_chunk = std::min(waiting, buffer_size); + int total = readSome(h, buf, first_chunk, timeout_ms); + if (total < 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kReadError); + } + if (total == 0) + { + // tiny grace period similar to Linux implementation + total = readSome(h, buf, first_chunk, 10); + if (total < 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kReadError); + } + if (total == 0) + { + return 0; + } + } + + while (total < buffer_size) + { + if (!cpp_bindings_windows::detail::bytesWaiting(h, &waiting)) + { + return cpp_bindings_windows::detail::failWin32(error_callback, + cpp_core::StatusCodes::kGetStateError); + } + if (waiting <= 0) + { + break; + } + const int chunk = std::min(waiting, buffer_size - total); + const int got = readSome(h, buf + total, chunk, 0); + if (got <= 0) + { + break; + } + total += got; + } + + return total; + } + +} // extern "C" diff --git a/src/serial_write.cpp b/src/serial_write.cpp new file mode 100644 index 0000000..aec1c5d --- /dev/null +++ b/src/serial_write.cpp @@ -0,0 +1,110 @@ +#include +#include + +#include "detail/win32_helpers.hpp" + +#include + +namespace +{ +auto writeSome(HANDLE handle, const void *src, int size, int timeout_ms) -> int +{ + if (size <= 0) + { + return 0; + } + + OVERLAPPED ov = {}; + ov.hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (ov.hEvent == nullptr) + { + SetLastError(ERROR_NOT_ENOUGH_MEMORY); + return -1; + } + + DWORD bytes_written = 0; + const BOOL ok = WriteFile(handle, src, static_cast(size), &bytes_written, &ov); + if (ok != 0) + { + CloseHandle(ov.hEvent); + return static_cast(bytes_written); + } + + const DWORD err = GetLastError(); + if (err != ERROR_IO_PENDING) + { + CloseHandle(ov.hEvent); + SetLastError(err); + return -1; + } + + if (timeout_ms < 0) + { + timeout_ms = 0; + } + const DWORD wait_rc = WaitForSingleObject(ov.hEvent, static_cast(timeout_ms)); + if (wait_rc == WAIT_TIMEOUT) + { + CancelIoEx(handle, &ov); + CloseHandle(ov.hEvent); + return 0; + } + if (wait_rc != WAIT_OBJECT_0) + { + CancelIoEx(handle, &ov); + CloseHandle(ov.hEvent); + SetLastError(ERROR_GEN_FAILURE); + return -1; + } + + if (GetOverlappedResult(handle, &ov, &bytes_written, FALSE) == 0) + { + const DWORD err2 = GetLastError(); + CloseHandle(ov.hEvent); + SetLastError(err2); + return -1; + } + + CloseHandle(ov.hEvent); + return static_cast(bytes_written); +} +} // namespace + +extern "C" +{ + MODULE_API auto serialWrite(int64_t handle, const void *buffer, int buffer_size, int timeout_ms, int /*multiplier*/, + ErrorCallbackT error_callback) -> int + { + if (buffer == nullptr || buffer_size <= 0) + { + return cpp_bindings_windows::detail::failMsg(error_callback, cpp_core::StatusCodes::kBufferError, + "Invalid buffer or buffer_size"); + } + + if (handle <= 0 || handle > std::numeric_limits::max()) + { + return cpp_bindings_windows::detail::failMsg( + error_callback, cpp_core::StatusCodes::kInvalidHandleError, "Invalid handle"); + } + + const HANDLE h = reinterpret_cast(static_cast(handle)); + if (h == nullptr || h == INVALID_HANDLE_VALUE) + { + return cpp_bindings_windows::detail::failMsg( + error_callback, cpp_core::StatusCodes::kInvalidHandleError, "Invalid handle"); + } + + const int written = writeSome(h, buffer, buffer_size, timeout_ms); + if (written < 0) + { + return cpp_bindings_windows::detail::failWin32(error_callback, cpp_core::StatusCodes::kWriteError); + } + + // Best-effort drain (similar to tcdrain). This may block; callers can keep + // timeouts small. + FlushFileBuffers(h); + + return written; + } + +} // extern "C" diff --git a/src/test.cpp b/src/test.cpp deleted file mode 100644 index 101b1ca..0000000 --- a/src/test.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include - -extern "C" __declspec(dllexport) int TestFunction() { - return 42; -} - -BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) { - return TRUE; -} diff --git a/tests/test_serial_arduino.cpp b/tests/test_serial_arduino.cpp new file mode 100644 index 0000000..2974eee --- /dev/null +++ b/tests/test_serial_arduino.cpp @@ -0,0 +1,160 @@ +#include +#include +#include +#include +#include +#include + +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include + +#include +#include +#include + +namespace +{ +auto widenUtf8(const char *s) -> std::wstring +{ + if (s == nullptr || s[0] == '\0') + { + return {}; + } + + const int needed = MultiByteToWideChar(CP_UTF8, 0, s, -1, nullptr, 0); + if (needed <= 0) + { + return {}; + } + + std::wstring out; + out.resize(static_cast(needed)); + const int written = MultiByteToWideChar(CP_UTF8, 0, s, -1, out.data(), needed); + if (written <= 0) + { + return {}; + } + + // drop trailing null terminator for convenience + if (!out.empty() && out.back() == L'\0') + { + out.pop_back(); + } + return out; +} +} // namespace + +class SerialArduinoTest : public ::testing::Test +{ + protected: + void SetUp() override + { + const char *env_port = std::getenv("SERIAL_TEST_PORT"); + std::wstring port_w = widenUtf8(env_port); + if (port_w.empty()) + { + port_w = L"COM3"; + } + + handle_ = serialOpen(const_cast(static_cast(port_w.c_str())), 115200, 8, 0, 0, nullptr); + if (handle_ <= 0) + { + GTEST_SKIP() << "Could not open serial port. Set SERIAL_TEST_PORT (e.g. " + "COM3) or connect Arduino."; + } + + // Arduino resets on open; wait a bit. + Sleep(2000); + port_w_ = std::move(port_w); + } + + void TearDown() override + { + if (handle_ > 0) + { + serialClose(handle_, nullptr); + handle_ = 0; + } + } + + intptr_t handle_ = 0; + std::wstring port_w_; +}; + +TEST_F(SerialArduinoTest, OpenClose) +{ + EXPECT_GT(handle_, 0) << "serialOpen should return a positive handle"; +} + +TEST_F(SerialArduinoTest, WriteReadEcho) +{ + const char *test_message = "Hello Arduino!\n"; + const int message_len = static_cast(strlen(test_message)); + + const int written = serialWrite(handle_, test_message, message_len, 1000, 1, nullptr); + EXPECT_EQ(written, message_len) << "Should write all bytes. Written: " << written << ", Expected: " << message_len; + + Sleep(500); + + char read_buffer[256] = {0}; + const int read_bytes = + serialRead(handle_, read_buffer, static_cast(sizeof(read_buffer) - 1), 2000, 1, nullptr); + + EXPECT_GT(read_bytes, 0) << "Should read at least some bytes"; + EXPECT_LE(read_bytes, static_cast(sizeof(read_buffer) - 1)) << "Should not overflow buffer"; + + read_buffer[read_bytes] = '\0'; + EXPECT_STRNE(read_buffer, "") << "Should receive echo from Arduino"; +} + +TEST_F(SerialArduinoTest, MultipleEchoCycles) +{ + const char *messages[] = {"Test1\n", "Test2\n", "Test3\n"}; + const int num_messages = 3; + + for (int i = 0; i < num_messages; ++i) + { + const int msg_len = static_cast(strlen(messages[i])); + + const int written = serialWrite(handle_, messages[i], msg_len, 1000, 1, nullptr); + EXPECT_EQ(written, msg_len) << "Cycle " << i << ": write failed"; + + Sleep(500); + + char read_buffer[256] = {0}; + const int read_bytes = + serialRead(handle_, read_buffer, static_cast(sizeof(read_buffer) - 1), 2000, 1, nullptr); + EXPECT_GT(read_bytes, 0) << "Cycle " << i << ": read failed"; + } +} + +TEST_F(SerialArduinoTest, ReadTimeout) +{ + char buffer[256]; + const int read_bytes = serialRead(handle_, buffer, static_cast(sizeof(buffer)), 100, 1, nullptr); + EXPECT_GE(read_bytes, 0) << "Timeout should return 0, not error"; +} + +TEST(SerialInvalidHandleTest, InvalidHandleRead) +{ + char buffer[256]; + const int result = serialRead(-1, buffer, static_cast(sizeof(buffer)), 1000, 1, nullptr); + EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)) + << "Should return error for invalid handle"; +} + +TEST(SerialInvalidHandleTest, InvalidHandleWrite) +{ + const char *data = "test"; + const int result = serialWrite(-1, data, 4, 1000, 1, nullptr); + EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)) + << "Should return error for invalid handle"; +} + +TEST(SerialInvalidHandleTest, InvalidHandleClose) +{ + const int result = serialClose(-1, nullptr); + EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kSuccess)); +} From 3273a450d186936aae32456e7a3e184317ad9a98 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Sun, 21 Dec 2025 09:37:12 +0100 Subject: [PATCH 2/9] Add compile definitions for Windows bindings in CMake configuration --- CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7333a96..fdbffb1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -98,6 +98,11 @@ target_link_libraries( cpp_core::cpp_core ) +# cpp-core's `MODULE_API` macro checks for `cpp_windows_bindings_EXPORTS` on Windows. +# Our target is named `cpp_bindings_windows`, so CMake would otherwise define +# `cpp_bindings_windows_EXPORTS` and `MODULE_API` would resolve to dllimport. +target_compile_definitions(cpp_bindings_windows PRIVATE cpp_windows_bindings_EXPORTS) + target_compile_features(cpp_bindings_windows PUBLIC cxx_std_23) # Collect all test source files From a21dc7822315e37540a721582a358beff9759f72 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Sun, 21 Dec 2025 09:41:31 +0100 Subject: [PATCH 3/9] Disable Google Mock in CMake configuration for Windows bindings --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fdbffb1..ed4cfab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,6 +66,7 @@ CPMAddPackage( OPTIONS "INSTALL_GTEST OFF" "gtest_force_shared_crt ON" + "BUILD_GMOCK OFF" ) include(CTest) @@ -126,7 +127,6 @@ if(TEST_SOURCES) cpp_bindings_windows GTest::gtest GTest::gtest_main - GTest::gmock ) target_compile_features(cpp_bindings_windows_tests PRIVATE cxx_std_23) From 271e1856285b743b474ea44600200e0837b39a21 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Sun, 21 Dec 2025 10:09:24 +0100 Subject: [PATCH 4/9] Remove embed_binary.sh script and refactor applyLineSettings function to return structured error messages in serial_open.cpp and serial_read.cpp. --- jsr/scripts/embed_binary.sh | 35 ------------------------------- src/serial_open.cpp | 41 ++++++++++++++++++++++++------------- src/serial_read.cpp | 7 +++++++ 3 files changed, 34 insertions(+), 49 deletions(-) delete mode 100755 jsr/scripts/embed_binary.sh diff --git a/jsr/scripts/embed_binary.sh b/jsr/scripts/embed_binary.sh deleted file mode 100755 index 2f500dd..0000000 --- a/jsr/scripts/embed_binary.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/sh -set -eu - -if [ "$#" -ne 3 ]; then - echo "Usage: $0 " >&2 - exit 1 -fi - -BINARY_PATH=$1 -JSR_BIN_PATH=$2 -TARGET=$3 - -if [ ! -f "$BINARY_PATH" ]; then - echo "Error: Binary path is not a file: $BINARY_PATH" >&2 - exit 1 -fi - -FILENAME=$(basename "$BINARY_PATH") - -mkdir -p "$JSR_BIN_PATH" - -cp "$BINARY_PATH" "$JSR_BIN_PATH/x86_64.so" - -BASE64_DATA=$(base64 "$BINARY_PATH" | tr -d '\n') - -jq -n \ - --arg target "$TARGET" \ - --arg filename "$FILENAME" \ - --arg data "$BASE64_DATA" \ - '{ - target: $target, - filename: $filename, - encoding: "base64", - data: $data - }' > "$JSR_BIN_PATH/x86_64.json" diff --git a/src/serial_open.cpp b/src/serial_open.cpp index 4e9d1e4..bf4207a 100644 --- a/src/serial_open.cpp +++ b/src/serial_open.cpp @@ -25,14 +25,22 @@ auto normalizePortPath(const wchar_t *port) -> std::wstring return p; } -auto applyLineSettings(HANDLE handle, int baudrate, int data_bits, int parity, int stop_bits) -> bool +struct ApplyLineSettingsResult +{ + cpp_core::StatusCodes code; + std::string message; +}; + +auto applyLineSettings(HANDLE handle, int baudrate, int data_bits, int parity, int stop_bits) -> ApplyLineSettingsResult { DCB dcb = {}; dcb.DCBlength = sizeof(DCB); if (GetCommState(handle, &dcb) == 0) { - return false; + const DWORD err = GetLastError(); + return {.code = cpp_core::StatusCodes::kGetStateError, + .message = "GetCommState failed: " + cpp_bindings_windows::detail::win32ErrorToString(err)}; } dcb.BaudRate = static_cast(baudrate); @@ -65,7 +73,7 @@ auto applyLineSettings(HANDLE handle, int baudrate, int data_bits, int parity, i break; default: SetLastError(ERROR_INVALID_PARAMETER); - return false; + return {.code = cpp_core::StatusCodes::kSetStateError, .message = "Invalid parity"}; } // stop_bits mapping: @@ -82,12 +90,14 @@ auto applyLineSettings(HANDLE handle, int baudrate, int data_bits, int parity, i else { SetLastError(ERROR_INVALID_PARAMETER); - return false; + return {.code = cpp_core::StatusCodes::kSetStateError, .message = "Invalid stop bits: must be 0, 1, or 2"}; } if (SetCommState(handle, &dcb) == 0) { - return false; + const DWORD err = GetLastError(); + return {.code = cpp_core::StatusCodes::kSetStateError, + .message = "SetCommState failed: " + cpp_bindings_windows::detail::win32ErrorToString(err)}; } // Use overlapped operations + explicit waits in read/write, so keep @@ -95,10 +105,12 @@ auto applyLineSettings(HANDLE handle, int baudrate, int data_bits, int parity, i COMMTIMEOUTS timeouts = {}; if (SetCommTimeouts(handle, &timeouts) == 0) { - return false; + const DWORD err = GetLastError(); + return {.code = cpp_core::StatusCodes::kSetTimeoutError, + .message = "SetCommTimeouts failed: " + cpp_bindings_windows::detail::win32ErrorToString(err)}; } - return true; + return {.code = cpp_core::StatusCodes::kSuccess, .message = ""}; } } // namespace @@ -139,18 +151,19 @@ extern "C" cpp_core::StatusCodes::kNotFoundError); } - if (!applyLineSettings(handle.get(), baudrate, data_bits, parity, stop_bits)) + const ApplyLineSettingsResult settings = + applyLineSettings(handle.get(), baudrate, data_bits, parity, stop_bits); + if (settings.code != cpp_core::StatusCodes::kSuccess) { - const cpp_core::StatusCodes code = (GetLastError() == ERROR_INVALID_PARAMETER) - ? cpp_core::StatusCodes::kSetStateError - : cpp_core::StatusCodes::kSetStateError; - return cpp_bindings_windows::detail::failWin32(error_callback, code); + const char *msg = settings.message.empty() ? "Failed to configure serial port" : settings.message.c_str(); + return cpp_bindings_windows::detail::failMsg(error_callback, settings.code, msg); } // Clear buffers (best-effort) PurgeComm(handle.get(), PURGE_RXCLEAR | PURGE_TXCLEAR | PURGE_RXABORT | PURGE_TXABORT); - const intptr_t out = reinterpret_cast(handle.release()); + // Validate before releasing ownership to avoid any chance of leaking the handle. + const intptr_t out = reinterpret_cast(handle.get()); if (out <= 0) { // Extremely unlikely, but keep the API contract: success -> positive @@ -158,7 +171,7 @@ extern "C" return cpp_bindings_windows::detail::failMsg( error_callback, cpp_core::StatusCodes::kInvalidHandleError, "Invalid handle generated"); } - return out; + return reinterpret_cast(handle.release()); } } // extern "C" diff --git a/src/serial_read.cpp b/src/serial_read.cpp index 0f438f5..b9a5e68 100644 --- a/src/serial_read.cpp +++ b/src/serial_read.cpp @@ -30,6 +30,13 @@ auto waitForRxChar(HANDLE handle, int timeout_ms) -> int DWORD mask = 0; const BOOL ok = WaitCommEvent(handle, &mask, &ov); + if (ok != 0) + { + // Completed synchronously. With an OVERLAPPED call the event is not guaranteed + // to be signaled in this case, so don't wait. + CloseHandle(ov.hEvent); + return 1; + } if (ok == 0) { const DWORD err = GetLastError(); From e1fb4c743f5f350879c122d0f632766b2cadc13b Mon Sep 17 00:00:00 2001 From: Katze719 Date: Sun, 21 Dec 2025 10:11:32 +0100 Subject: [PATCH 5/9] Add readExact function to improve serial reading in tests; update expectations in SerialArduinoTest --- tests/test_serial_arduino.cpp | 56 +++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/tests/test_serial_arduino.cpp b/tests/test_serial_arduino.cpp index 2974eee..881ec05 100644 --- a/tests/test_serial_arduino.cpp +++ b/tests/test_serial_arduino.cpp @@ -13,6 +13,7 @@ #include #include #include +#include namespace { @@ -44,6 +45,42 @@ auto widenUtf8(const char *s) -> std::wstring } return out; } + +auto readExact(intptr_t handle, char *dst, int want_bytes, int total_timeout_ms) -> int +{ + if (dst == nullptr || want_bytes <= 0) + { + return 0; + } + + const ULONGLONG start = GetTickCount64(); + int total = 0; + while (total < want_bytes) + { + const ULONGLONG now = GetTickCount64(); + const int elapsed = static_cast(now - start); + if (elapsed >= total_timeout_ms) + { + break; + } + + // Read remaining bytes with a small per-call timeout to make progress. + const int remaining = want_bytes - total; + const int chunk = serialRead(handle, dst + total, remaining, 200, 1, nullptr); + if (chunk < 0) + { + return chunk; + } + if (chunk == 0) + { + Sleep(10); + continue; + } + total += chunk; + } + + return total; +} } // namespace class SerialArduinoTest : public ::testing::Test @@ -99,14 +136,13 @@ TEST_F(SerialArduinoTest, WriteReadEcho) Sleep(500); char read_buffer[256] = {0}; - const int read_bytes = - serialRead(handle_, read_buffer, static_cast(sizeof(read_buffer) - 1), 2000, 1, nullptr); + const int read_bytes = readExact(handle_, read_buffer, message_len, 3000); EXPECT_GT(read_bytes, 0) << "Should read at least some bytes"; - EXPECT_LE(read_bytes, static_cast(sizeof(read_buffer) - 1)) << "Should not overflow buffer"; - - read_buffer[read_bytes] = '\0'; - EXPECT_STRNE(read_buffer, "") << "Should receive echo from Arduino"; + EXPECT_EQ(read_bytes, message_len) << "Should read exactly the echoed message length"; + EXPECT_EQ(std::string_view(read_buffer, static_cast(message_len)), + std::string_view(test_message, static_cast(message_len))) + << "Echoed content should match what was sent"; } TEST_F(SerialArduinoTest, MultipleEchoCycles) @@ -124,9 +160,11 @@ TEST_F(SerialArduinoTest, MultipleEchoCycles) Sleep(500); char read_buffer[256] = {0}; - const int read_bytes = - serialRead(handle_, read_buffer, static_cast(sizeof(read_buffer) - 1), 2000, 1, nullptr); - EXPECT_GT(read_bytes, 0) << "Cycle " << i << ": read failed"; + const int read_bytes = readExact(handle_, read_buffer, msg_len, 3000); + EXPECT_EQ(read_bytes, msg_len) << "Cycle " << i << ": read size mismatch"; + EXPECT_EQ(std::string_view(read_buffer, static_cast(msg_len)), + std::string_view(messages[i], static_cast(msg_len))) + << "Cycle " << i << ": echo content mismatch"; } } From eecf639c07b0df32f9cb88220c27a723b3585de6 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Mon, 22 Dec 2025 12:32:29 +0100 Subject: [PATCH 6/9] Add virtual COM port setup in GitHub workflows and update SerialArduinoTest to use COM5. This includes installing the com0com driver, creating a virtual COM pair, and starting an echo loop for testing. Adjusted the default serial port in tests to COM5 and improved error handling for port opening. --- .github/workflows/cpp-tests.yml | 44 ++++++++++++++++++++++++++++++++ .github/workflows/deno-tests.yml | 44 ++++++++++++++++++++++++++++++++ tests/test_serial_arduino.cpp | 9 +++---- 3 files changed, 91 insertions(+), 6 deletions(-) diff --git a/.github/workflows/cpp-tests.yml b/.github/workflows/cpp-tests.yml index 88ae616..dea25cc 100644 --- a/.github/workflows/cpp-tests.yml +++ b/.github/workflows/cpp-tests.yml @@ -14,6 +14,48 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Install virtual COM port driver (com0com) + shell: pwsh + run: | + choco install com0com --yes --no-progress + + - name: Create virtual COM pair and start echo + shell: pwsh + run: | + $setupc = $null + $candidates = @( + "${env:ProgramFiles(x86)}\com0com\setupc.exe", + "${env:ProgramFiles}\com0com\setupc.exe" + ) + foreach ($p in $candidates) { if (Test-Path $p) { $setupc = $p; break } } + if (-not $setupc) { throw "setupc.exe not found after com0com install" } + + # Create COM5<->COM6 + & $setupc install PortName=COM5 PortName=COM6 | Out-Host + + # Start an echo loop on COM6 in the background + $echoScript = @' + Add-Type -AssemblyName System + $sp = New-Object System.IO.Ports.SerialPort "COM6", 115200, "None", 8, "One" + $sp.ReadTimeout = 500 + $sp.WriteTimeout = 500 + $sp.Open() + try { + while ($true) { + try { + $b = $sp.ReadByte() + if ($b -ge 0) { $sp.BaseStream.WriteByte([byte]$b); $sp.BaseStream.Flush() } + } catch [System.TimeoutException] { + continue + } + } + } finally { + $sp.Close() + } + '@ + Set-Content -Path echo.ps1 -Value $echoScript -Encoding utf8 + Start-Process pwsh -ArgumentList "-NoProfile","-ExecutionPolicy","Bypass","-File","echo.ps1" -WindowStyle Hidden + - name: Setup CMake >= 3.30 uses: jwlawson/actions-setup-cmake@v2 with: @@ -28,6 +70,8 @@ jobs: cmake --build build --config Release - name: Run C++ unit/integration tests + env: + SERIAL_TEST_PORT: COM5 run: | ctest --test-dir build -C Release --output-on-failure diff --git a/.github/workflows/deno-tests.yml b/.github/workflows/deno-tests.yml index e634bdb..433f855 100644 --- a/.github/workflows/deno-tests.yml +++ b/.github/workflows/deno-tests.yml @@ -18,6 +18,48 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Install virtual COM port driver (com0com) + shell: pwsh + run: | + choco install com0com --yes --no-progress + + - name: Create virtual COM pair and start echo + shell: pwsh + run: | + $setupc = $null + $candidates = @( + "${env:ProgramFiles(x86)}\com0com\setupc.exe", + "${env:ProgramFiles}\com0com\setupc.exe" + ) + foreach ($p in $candidates) { if (Test-Path $p) { $setupc = $p; break } } + if (-not $setupc) { throw "setupc.exe not found after com0com install" } + + # Create COM5<->COM6 + & $setupc install PortName=COM5 PortName=COM6 | Out-Host + + # Start an echo loop on COM6 in the background + $echoScript = @' + Add-Type -AssemblyName System + $sp = New-Object System.IO.Ports.SerialPort "COM6", 115200, "None", 8, "One" + $sp.ReadTimeout = 500 + $sp.WriteTimeout = 500 + $sp.Open() + try { + while ($true) { + try { + $b = $sp.ReadByte() + if ($b -ge 0) { $sp.BaseStream.WriteByte([byte]$b); $sp.BaseStream.Flush() } + } catch [System.TimeoutException] { + continue + } + } + } finally { + $sp.Close() + } + '@ + Set-Content -Path echo.ps1 -Value $echoScript -Encoding utf8 + Start-Process pwsh -ArgumentList "-NoProfile","-ExecutionPolicy","Bypass","-File","echo.ps1" -WindowStyle Hidden + - name: Setup CMake >= 3.30 uses: jwlawson/actions-setup-cmake@v2 with: @@ -38,6 +80,8 @@ jobs: - name: Run Deno integration tests working-directory: integration_tests + env: + SERIAL_TEST_PORT: COM5 run: | deno task test diff --git a/tests/test_serial_arduino.cpp b/tests/test_serial_arduino.cpp index 881ec05..e764324 100644 --- a/tests/test_serial_arduino.cpp +++ b/tests/test_serial_arduino.cpp @@ -92,15 +92,12 @@ class SerialArduinoTest : public ::testing::Test std::wstring port_w = widenUtf8(env_port); if (port_w.empty()) { - port_w = L"COM3"; + port_w = L"COM5"; } handle_ = serialOpen(const_cast(static_cast(port_w.c_str())), 115200, 8, 0, 0, nullptr); - if (handle_ <= 0) - { - GTEST_SKIP() << "Could not open serial port. Set SERIAL_TEST_PORT (e.g. " - "COM3) or connect Arduino."; - } + ASSERT_GT(handle_, 0) << "Could not open serial port. Set SERIAL_TEST_PORT (e.g. COM5) or provide a working " + "virtual/real serial device."; // Arduino resets on open; wait a bit. Sleep(2000); From cb72296bc05d44208a978138b7969f6c3be6d1f3 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Mon, 22 Dec 2025 12:35:52 +0100 Subject: [PATCH 7/9] Refactor virtual COM port setup in GitHub workflows to enhance error handling and improve script execution. Updated paths for locating setupc.exe and com0com.inf, and added a sleep delay after starting the echo loop for stability. --- .github/workflows/cpp-tests.yml | 25 ++++++++++++++++--------- .github/workflows/deno-tests.yml | 25 ++++++++++++++++--------- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/.github/workflows/cpp-tests.yml b/.github/workflows/cpp-tests.yml index dea25cc..61a208e 100644 --- a/.github/workflows/cpp-tests.yml +++ b/.github/workflows/cpp-tests.yml @@ -22,20 +22,26 @@ jobs: - name: Create virtual COM pair and start echo shell: pwsh run: | - $setupc = $null - $candidates = @( - "${env:ProgramFiles(x86)}\com0com\setupc.exe", - "${env:ProgramFiles}\com0com\setupc.exe" - ) - foreach ($p in $candidates) { if (Test-Path $p) { $setupc = $p; break } } - if (-not $setupc) { throw "setupc.exe not found after com0com install" } + $roots = @( + "C:\ProgramData\chocolatey\lib", + "${env:ProgramFiles}", + "${env:ProgramFiles(x86)}" + ) | Where-Object { $_ -and (Test-Path $_) } + + $setupcItem = Get-ChildItem -Recurse -File -Filter setupc.exe -ErrorAction SilentlyContinue $roots | Select-Object -First 1 + if (-not $setupcItem) { throw "setupc.exe not found after com0com install (searched: $($roots -join ', '))" } + + $infItem = Get-ChildItem -Recurse -File -Filter com0com.inf -ErrorAction SilentlyContinue $roots | Select-Object -First 1 + if (-not $infItem) { throw "com0com.inf not found after com0com install (searched: $($roots -join ', '))" } # Create COM5<->COM6 - & $setupc install PortName=COM5 PortName=COM6 | Out-Host + Push-Location $infItem.DirectoryName + & $setupcItem.FullName install PortName=COM5 PortName=COM6 | Out-Host + Pop-Location # Start an echo loop on COM6 in the background $echoScript = @' - Add-Type -AssemblyName System + Add-Type -AssemblyName System.IO.Ports $sp = New-Object System.IO.Ports.SerialPort "COM6", 115200, "None", 8, "One" $sp.ReadTimeout = 500 $sp.WriteTimeout = 500 @@ -55,6 +61,7 @@ jobs: '@ Set-Content -Path echo.ps1 -Value $echoScript -Encoding utf8 Start-Process pwsh -ArgumentList "-NoProfile","-ExecutionPolicy","Bypass","-File","echo.ps1" -WindowStyle Hidden + Start-Sleep -Seconds 2 - name: Setup CMake >= 3.30 uses: jwlawson/actions-setup-cmake@v2 diff --git a/.github/workflows/deno-tests.yml b/.github/workflows/deno-tests.yml index 433f855..e6b2106 100644 --- a/.github/workflows/deno-tests.yml +++ b/.github/workflows/deno-tests.yml @@ -26,20 +26,26 @@ jobs: - name: Create virtual COM pair and start echo shell: pwsh run: | - $setupc = $null - $candidates = @( - "${env:ProgramFiles(x86)}\com0com\setupc.exe", - "${env:ProgramFiles}\com0com\setupc.exe" - ) - foreach ($p in $candidates) { if (Test-Path $p) { $setupc = $p; break } } - if (-not $setupc) { throw "setupc.exe not found after com0com install" } + $roots = @( + "C:\ProgramData\chocolatey\lib", + "${env:ProgramFiles}", + "${env:ProgramFiles(x86)}" + ) | Where-Object { $_ -and (Test-Path $_) } + + $setupcItem = Get-ChildItem -Recurse -File -Filter setupc.exe -ErrorAction SilentlyContinue $roots | Select-Object -First 1 + if (-not $setupcItem) { throw "setupc.exe not found after com0com install (searched: $($roots -join ', '))" } + + $infItem = Get-ChildItem -Recurse -File -Filter com0com.inf -ErrorAction SilentlyContinue $roots | Select-Object -First 1 + if (-not $infItem) { throw "com0com.inf not found after com0com install (searched: $($roots -join ', '))" } # Create COM5<->COM6 - & $setupc install PortName=COM5 PortName=COM6 | Out-Host + Push-Location $infItem.DirectoryName + & $setupcItem.FullName install PortName=COM5 PortName=COM6 | Out-Host + Pop-Location # Start an echo loop on COM6 in the background $echoScript = @' - Add-Type -AssemblyName System + Add-Type -AssemblyName System.IO.Ports $sp = New-Object System.IO.Ports.SerialPort "COM6", 115200, "None", 8, "One" $sp.ReadTimeout = 500 $sp.WriteTimeout = 500 @@ -59,6 +65,7 @@ jobs: '@ Set-Content -Path echo.ps1 -Value $echoScript -Encoding utf8 Start-Process pwsh -ArgumentList "-NoProfile","-ExecutionPolicy","Bypass","-File","echo.ps1" -WindowStyle Hidden + Start-Sleep -Seconds 2 - name: Setup CMake >= 3.30 uses: jwlawson/actions-setup-cmake@v2 From 7194e8a153f8c1e1ba0693a939ba86c1d171ebb0 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Mon, 22 Dec 2025 12:40:02 +0100 Subject: [PATCH 8/9] Refactor virtual COM port setup in Deno and C++ GitHub workflows to improve path handling and error reporting. Updated script to search for setupc.exe and com0com.inf exclusively within the Chocolatey package directory, enhancing efficiency and reliability. --- .github/workflows/cpp-tests.yml | 17 ++++++++--------- .github/workflows/deno-tests.yml | 17 ++++++++--------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/.github/workflows/cpp-tests.yml b/.github/workflows/cpp-tests.yml index 61a208e..0bfdb26 100644 --- a/.github/workflows/cpp-tests.yml +++ b/.github/workflows/cpp-tests.yml @@ -22,17 +22,16 @@ jobs: - name: Create virtual COM pair and start echo shell: pwsh run: | - $roots = @( - "C:\ProgramData\chocolatey\lib", - "${env:ProgramFiles}", - "${env:ProgramFiles(x86)}" - ) | Where-Object { $_ -and (Test-Path $_) } + if (-not $env:ChocolateyInstall) { $env:ChocolateyInstall = "C:\ProgramData\chocolatey" } + $pkgRoot = Join-Path $env:ChocolateyInstall "lib\com0com" + if (-not (Test-Path $pkgRoot)) { throw "com0com package root not found at $pkgRoot" } - $setupcItem = Get-ChildItem -Recurse -File -Filter setupc.exe -ErrorAction SilentlyContinue $roots | Select-Object -First 1 - if (-not $setupcItem) { throw "setupc.exe not found after com0com install (searched: $($roots -join ', '))" } + # Avoid searching huge directories (Program Files). Only look inside the Chocolatey package folder. + $setupcItem = Get-ChildItem -Recurse -File -Filter setupc.exe -ErrorAction Stop $pkgRoot | Select-Object -First 1 + if (-not $setupcItem) { throw "setupc.exe not found under $pkgRoot" } - $infItem = Get-ChildItem -Recurse -File -Filter com0com.inf -ErrorAction SilentlyContinue $roots | Select-Object -First 1 - if (-not $infItem) { throw "com0com.inf not found after com0com install (searched: $($roots -join ', '))" } + $infItem = Get-ChildItem -Recurse -File -Filter com0com.inf -ErrorAction Stop $pkgRoot | Select-Object -First 1 + if (-not $infItem) { throw "com0com.inf not found under $pkgRoot" } # Create COM5<->COM6 Push-Location $infItem.DirectoryName diff --git a/.github/workflows/deno-tests.yml b/.github/workflows/deno-tests.yml index e6b2106..0c0cc46 100644 --- a/.github/workflows/deno-tests.yml +++ b/.github/workflows/deno-tests.yml @@ -26,17 +26,16 @@ jobs: - name: Create virtual COM pair and start echo shell: pwsh run: | - $roots = @( - "C:\ProgramData\chocolatey\lib", - "${env:ProgramFiles}", - "${env:ProgramFiles(x86)}" - ) | Where-Object { $_ -and (Test-Path $_) } + if (-not $env:ChocolateyInstall) { $env:ChocolateyInstall = "C:\ProgramData\chocolatey" } + $pkgRoot = Join-Path $env:ChocolateyInstall "lib\com0com" + if (-not (Test-Path $pkgRoot)) { throw "com0com package root not found at $pkgRoot" } - $setupcItem = Get-ChildItem -Recurse -File -Filter setupc.exe -ErrorAction SilentlyContinue $roots | Select-Object -First 1 - if (-not $setupcItem) { throw "setupc.exe not found after com0com install (searched: $($roots -join ', '))" } + # Avoid searching huge directories (Program Files). Only look inside the Chocolatey package folder. + $setupcItem = Get-ChildItem -Recurse -File -Filter setupc.exe -ErrorAction Stop $pkgRoot | Select-Object -First 1 + if (-not $setupcItem) { throw "setupc.exe not found under $pkgRoot" } - $infItem = Get-ChildItem -Recurse -File -Filter com0com.inf -ErrorAction SilentlyContinue $roots | Select-Object -First 1 - if (-not $infItem) { throw "com0com.inf not found after com0com install (searched: $($roots -join ', '))" } + $infItem = Get-ChildItem -Recurse -File -Filter com0com.inf -ErrorAction Stop $pkgRoot | Select-Object -First 1 + if (-not $infItem) { throw "com0com.inf not found under $pkgRoot" } # Create COM5<->COM6 Push-Location $infItem.DirectoryName From 2a128d28d5616777777066f6ec2a62fce56baf43 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Mon, 22 Dec 2025 12:42:04 +0100 Subject: [PATCH 9/9] Enhance virtual COM port setup in GitHub workflows by implementing a more efficient and deterministic search for setupc.exe and com0com.inf. Updated error handling to check multiple candidate paths and limit search depth, improving reliability in locating necessary files. --- .github/workflows/cpp-tests.yml | 32 +++++++++++++++++++++++++----- .github/workflows/deno-tests.yml | 34 ++++++++++++++++++++++++++------ 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/.github/workflows/cpp-tests.yml b/.github/workflows/cpp-tests.yml index 0bfdb26..9232e8d 100644 --- a/.github/workflows/cpp-tests.yml +++ b/.github/workflows/cpp-tests.yml @@ -26,12 +26,34 @@ jobs: $pkgRoot = Join-Path $env:ChocolateyInstall "lib\com0com" if (-not (Test-Path $pkgRoot)) { throw "com0com package root not found at $pkgRoot" } - # Avoid searching huge directories (Program Files). Only look inside the Chocolatey package folder. - $setupcItem = Get-ChildItem -Recurse -File -Filter setupc.exe -ErrorAction Stop $pkgRoot | Select-Object -First 1 - if (-not $setupcItem) { throw "setupc.exe not found under $pkgRoot" } + # Fast, deterministic search for setupc.exe and com0com.inf (avoid full disk recursion). + $setupCandidates = @( + (Join-Path $pkgRoot "tools\\setupc.exe"), + (Join-Path $pkgRoot "tools\\com0com\\setupc.exe"), + "${env:ProgramFiles(x86)}\\com0com\\setupc.exe", + "${env:ProgramFiles}\\com0com\\setupc.exe" + ) | Where-Object { $_ -and (Test-Path $_) } - $infItem = Get-ChildItem -Recurse -File -Filter com0com.inf -ErrorAction Stop $pkgRoot | Select-Object -First 1 - if (-not $infItem) { throw "com0com.inf not found under $pkgRoot" } + $setupcItem = $null + foreach ($p in $setupCandidates) { $setupcItem = Get-Item -LiteralPath $p; break } + if (-not $setupcItem) { + $setupcItem = Get-ChildItem -Recurse -Depth 6 -File -Filter setupc.exe -ErrorAction SilentlyContinue $pkgRoot | Select-Object -First 1 + } + if (-not $setupcItem) { throw "setupc.exe not found (checked candidates and searched depth<=6 under $pkgRoot)" } + + $infCandidates = @( + (Join-Path $pkgRoot "tools\\com0com.inf"), + (Join-Path $pkgRoot "tools\\com0com\\com0com.inf"), + (Join-Path $setupcItem.DirectoryName "com0com.inf"), + "C:\\Windows\\INF\\com0com.inf" + ) | Where-Object { $_ -and (Test-Path $_) } + + $infItem = $null + foreach ($p in $infCandidates) { $infItem = Get-Item -LiteralPath $p; break } + if (-not $infItem) { + $infItem = Get-ChildItem -Recurse -Depth 6 -File -Filter com0com.inf -ErrorAction SilentlyContinue $pkgRoot | Select-Object -First 1 + } + if (-not $infItem) { throw "com0com.inf not found (checked candidates and searched depth<=6 under $pkgRoot)" } # Create COM5<->COM6 Push-Location $infItem.DirectoryName diff --git a/.github/workflows/deno-tests.yml b/.github/workflows/deno-tests.yml index 0c0cc46..0fcd21b 100644 --- a/.github/workflows/deno-tests.yml +++ b/.github/workflows/deno-tests.yml @@ -30,12 +30,34 @@ jobs: $pkgRoot = Join-Path $env:ChocolateyInstall "lib\com0com" if (-not (Test-Path $pkgRoot)) { throw "com0com package root not found at $pkgRoot" } - # Avoid searching huge directories (Program Files). Only look inside the Chocolatey package folder. - $setupcItem = Get-ChildItem -Recurse -File -Filter setupc.exe -ErrorAction Stop $pkgRoot | Select-Object -First 1 - if (-not $setupcItem) { throw "setupc.exe not found under $pkgRoot" } - - $infItem = Get-ChildItem -Recurse -File -Filter com0com.inf -ErrorAction Stop $pkgRoot | Select-Object -First 1 - if (-not $infItem) { throw "com0com.inf not found under $pkgRoot" } + # Fast, deterministic search for setupc.exe and com0com.inf (avoid full disk recursion). + $setupCandidates = @( + (Join-Path $pkgRoot "tools\\setupc.exe"), + (Join-Path $pkgRoot "tools\\com0com\\setupc.exe"), + "${env:ProgramFiles(x86)}\\com0com\\setupc.exe", + "${env:ProgramFiles}\\com0com\\setupc.exe" + ) | Where-Object { $_ -and (Test-Path $_) } + + $setupcItem = $null + foreach ($p in $setupCandidates) { $setupcItem = Get-Item -LiteralPath $p; break } + if (-not $setupcItem) { + $setupcItem = Get-ChildItem -Recurse -Depth 6 -File -Filter setupc.exe -ErrorAction SilentlyContinue $pkgRoot | Select-Object -First 1 + } + if (-not $setupcItem) { throw "setupc.exe not found (checked candidates and searched depth<=6 under $pkgRoot)" } + + $infCandidates = @( + (Join-Path $pkgRoot "tools\\com0com.inf"), + (Join-Path $pkgRoot "tools\\com0com\\com0com.inf"), + (Join-Path $setupcItem.DirectoryName "com0com.inf"), + "C:\\Windows\\INF\\com0com.inf" + ) | Where-Object { $_ -and (Test-Path $_) } + + $infItem = $null + foreach ($p in $infCandidates) { $infItem = Get-Item -LiteralPath $p; break } + if (-not $infItem) { + $infItem = Get-ChildItem -Recurse -Depth 6 -File -Filter com0com.inf -ErrorAction SilentlyContinue $pkgRoot | Select-Object -First 1 + } + if (-not $infItem) { throw "com0com.inf not found (checked candidates and searched depth<=6 under $pkgRoot)" } # Create COM5<->COM6 Push-Location $infItem.DirectoryName