From 9523e745223af0876c1322647be886bc582a510d Mon Sep 17 00:00:00 2001 From: Scott Bailey Date: Thu, 26 Jan 2023 15:07:53 -0600 Subject: [PATCH] Initial development. Incomplete, but ready for a merge into main. --- .clang-format | 137 +++++ .gitignore | 3 +- CMakeLists.txt | 25 + aproj/CMakeLists.txt | 21 + aproj/aproj-add-app.cpp | 143 ++++++ aproj/aproj-add-dep.cpp | 201 ++++++++ aproj/aproj-add-lib.cpp | 140 ++++++ aproj/aproj-add-test.cpp | 122 +++++ aproj/aproj-common.h | 227 +++++++++ aproj/aproj-init.cpp | 155 ++++++ aproj/aproj-populate.cpp | 70 +++ aproj/aproj-rm-dep.cpp | 267 ++++++++++ aproj/aproj-update-dep.cpp | 249 +++++++++ aproj/aproj-validate.cpp | 69 +++ aproj/aproj.cpp | 156 ++++++ common.cmake | 129 +++++ common/CMakeLists.txt | 27 + common/antler/common/so_support.h | 32 ++ common/antler/string/detail/from.ipp | 36 ++ common/antler/string/detail/split.ipp | 30 ++ common/antler/string/detail/trim.ipp | 36 ++ common/antler/string/from.h | 37 ++ common/antler/string/split.h | 20 + common/antler/string/trim.h | 20 + common/antler/system/detail/exec.ipp | 40 ++ common/antler/system/exec.h | 35 ++ .../sb/filesystem/detail/executable_path.ipp | 105 ++++ common/sb/filesystem/executable_path.h | 47 ++ common/sb/ignore.h | 13 + common/src/lib.cpp | 3 + common/src/so_support.h | 1 + depends/CMakeLists.txt | 13 + depends/patches/rapidyaml.patch | 69 +++ depends/patches/toml.patch | 32 ++ depends/patches/yaml.patch | 13 + depends/rapidyaml.cmake | 57 +++ depends/toml.cmake | 11 + depends/yaml-cpp.cmake | 11 + depends/yaml.cmake | 10 + docs/project_manager.md | 473 +++++++++--------- docs/sample1.yaml | 44 ++ docs/sample2.yaml | 35 ++ docs/sample3.yaml | 33 ++ project/CMakeLists.txt | 28 ++ project/antler/project/dependency.h | 91 ++++ project/antler/project/language.h | 36 ++ project/antler/project/location.h | 21 + project/antler/project/object.h | 97 ++++ project/antler/project/project.h | 134 +++++ project/antler/project/semver.h | 70 +++ project/antler/project/so_support.h | 22 + project/antler/project/version.h | 88 ++++ project/antler/project/version_compare.h | 29 ++ project/antler/project/version_constraint.h | 64 +++ project/src/cmake.cpp | 52 ++ project/src/cmake.h | 25 + project/src/dependency.cpp | 201 ++++++++ project/src/key.cpp | 84 ++++ project/src/key.h | 50 ++ project/src/langague.cpp | 84 ++++ project/src/location.cpp | 83 +++ project/src/object.cpp | 160 ++++++ project/src/project-is_valid.cpp | 26 + project/src/project-parse.cpp | 451 +++++++++++++++++ project/src/project-populate.cpp | 130 +++++ project/src/project-print.cpp | 240 +++++++++ project/src/project.cpp | 306 +++++++++++ project/src/semver.cpp | 245 +++++++++ project/src/test.cpp | 1 + project/src/version.cpp | 236 +++++++++ project/src/version_compare.cpp | 96 ++++ project/src/version_constraint.cpp | 263 ++++++++++ test/CMakeLists.txt | 16 + test/example.yaml | 28 ++ test/test_common.h | 30 ++ test/version.cpp | 112 +++++ test/version_constraint.cpp | 82 +++ test/version_semver.cpp | 80 +++ 78 files changed, 6923 insertions(+), 235 deletions(-) create mode 100644 .clang-format create mode 100644 CMakeLists.txt create mode 100644 aproj/CMakeLists.txt create mode 100644 aproj/aproj-add-app.cpp create mode 100644 aproj/aproj-add-dep.cpp create mode 100644 aproj/aproj-add-lib.cpp create mode 100644 aproj/aproj-add-test.cpp create mode 100644 aproj/aproj-common.h create mode 100644 aproj/aproj-init.cpp create mode 100644 aproj/aproj-populate.cpp create mode 100644 aproj/aproj-rm-dep.cpp create mode 100644 aproj/aproj-update-dep.cpp create mode 100644 aproj/aproj-validate.cpp create mode 100644 aproj/aproj.cpp create mode 100644 common.cmake create mode 100644 common/CMakeLists.txt create mode 100644 common/antler/common/so_support.h create mode 100644 common/antler/string/detail/from.ipp create mode 100644 common/antler/string/detail/split.ipp create mode 100644 common/antler/string/detail/trim.ipp create mode 100644 common/antler/string/from.h create mode 100644 common/antler/string/split.h create mode 100644 common/antler/string/trim.h create mode 100644 common/antler/system/detail/exec.ipp create mode 100644 common/antler/system/exec.h create mode 100644 common/sb/filesystem/detail/executable_path.ipp create mode 100644 common/sb/filesystem/executable_path.h create mode 100644 common/sb/ignore.h create mode 100644 common/src/lib.cpp create mode 100644 common/src/so_support.h create mode 100644 depends/CMakeLists.txt create mode 100644 depends/patches/rapidyaml.patch create mode 100644 depends/patches/toml.patch create mode 100644 depends/patches/yaml.patch create mode 100644 depends/rapidyaml.cmake create mode 100644 depends/toml.cmake create mode 100644 depends/yaml-cpp.cmake create mode 100644 depends/yaml.cmake create mode 100644 docs/sample1.yaml create mode 100644 docs/sample2.yaml create mode 100644 docs/sample3.yaml create mode 100644 project/CMakeLists.txt create mode 100644 project/antler/project/dependency.h create mode 100644 project/antler/project/language.h create mode 100644 project/antler/project/location.h create mode 100644 project/antler/project/object.h create mode 100644 project/antler/project/project.h create mode 100644 project/antler/project/semver.h create mode 100644 project/antler/project/so_support.h create mode 100644 project/antler/project/version.h create mode 100644 project/antler/project/version_compare.h create mode 100644 project/antler/project/version_constraint.h create mode 100644 project/src/cmake.cpp create mode 100644 project/src/cmake.h create mode 100644 project/src/dependency.cpp create mode 100644 project/src/key.cpp create mode 100644 project/src/key.h create mode 100644 project/src/langague.cpp create mode 100644 project/src/location.cpp create mode 100644 project/src/object.cpp create mode 100644 project/src/project-is_valid.cpp create mode 100644 project/src/project-parse.cpp create mode 100644 project/src/project-populate.cpp create mode 100644 project/src/project-print.cpp create mode 100644 project/src/project.cpp create mode 100644 project/src/semver.cpp create mode 100644 project/src/test.cpp create mode 100644 project/src/version.cpp create mode 100644 project/src/version_compare.cpp create mode 100644 project/src/version_constraint.cpp create mode 100644 test/CMakeLists.txt create mode 100644 test/example.yaml create mode 100644 test/test_common.h create mode 100644 test/version.cpp create mode 100644 test/version_constraint.cpp create mode 100644 test/version_semver.cpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..35027f3 --- /dev/null +++ b/.clang-format @@ -0,0 +1,137 @@ +--- +Language: Cpp +# BasedOnStyle: Mozilla +AccessModifierOffset: -3 +AlignAfterOpenBracket: Align +AlignArrayOfStructures: Left +AlignConsecutiveMacros: false +AlignConsecutiveAssignments: true +AlignConsecutiveDeclarations: true +AlignEscapedNewlines: Left +AlignOperands: true +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: true +AllowAllConstructorInitializersOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: Empty +AllowShortCaseLabelsOnASingleLine: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortLambdasOnASingleLine: All +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: Never + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Custom +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeComma +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeComma +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 0 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 3 +ContinuationIndentWidth: 3 +Cpp11BracedListStyle: false +DeriveLineEnding: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: false +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^"(llvm|llvm-c|clang|clang-c)/' + Priority: 2 + SortPriority: 0 + - Regex: '^(<|"(gtest|gmock|isl|json)/)' + Priority: 3 + SortPriority: 0 + - Regex: '.*' + Priority: 1 + SortPriority: 0 +IncludeIsMainRegex: '(Test)?$' +IncludeIsMainSourceRegex: '' +IndentCaseLabels: true +IndentGotoLabels: true +IndentPPDirectives: None +IndentWidth: 3 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 3 +NamespaceIndentation: None +ObjCBinPackProtocolList: Auto +ObjCBlockIndentWidth: 3 +ObjCSpaceAfterProperty: true +ObjCSpaceBeforeProtocolList: false +PenaltyBreakAssignment: 3 +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Left +ReflowComments: true +SortIncludes: false +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInConditionalStatement: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpaceBeforeSquareBrackets: false +Standard: Latest +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TabWidth: 8 +UseCRLF: false +UseTab: Never +... diff --git a/.gitignore b/.gitignore index 40a08d0..e1a6252 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ [Bb]uild*/* -CMakeLists.txt.user \ No newline at end of file +CMakeLists.txt.user +*.md.backup \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..f7f18b6 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.11) + +project("Antelope Project Management System") + +# Add dependent libraries. +# Make sure this happens BEFORE we include our common settings, +# otherwise we'll be compiling outside libraries with -Wall, +# -Wextra, -Werror +add_subdirectory( depends ) + +# Add some common stuff. +include( ./common.cmake REQUIRED ) + + +add_subdirectory( common ) +add_subdirectory( project ) +add_subdirectory( aproj ) +#add_subdirectory( aproj-qt ) + + +option(BUILD_TESTS "Build and run the tests." On) +if(BUILD_TESTS) + enable_testing() + add_subdirectory( test ) +endif() diff --git a/aproj/CMakeLists.txt b/aproj/CMakeLists.txt new file mode 100644 index 0000000..c8f212e --- /dev/null +++ b/aproj/CMakeLists.txt @@ -0,0 +1,21 @@ + +# +# These files are generally built in a common way, so here are some functions to simplify the process. +# + +function(add_aproj target_name files) + add_executable(${target_name} ${files}) + set_property(TARGET ${target_name} PROPERTY CXX_STANDARD 20) + target_link_libraries(${target_name} PUBLIC antler-project) + target_include_directories(${target_name} PRIVATE . ) + install(FILES $ COMPONENT Runtime DESTINATION .) +endfunction() + + +file(GLOB src_cpp ./*.cpp) + +foreach(file_name IN ITEMS ${src_cpp}) + cmake_path(GET file_name FILENAME fn) + string(REPLACE ".cpp" "" name ${fn}) + add_aproj("${name}" "${fn}") +endforeach() diff --git a/aproj/aproj-add-app.cpp b/aproj/aproj-add-app.cpp new file mode 100644 index 0000000..6eb5290 --- /dev/null +++ b/aproj/aproj-add-app.cpp @@ -0,0 +1,143 @@ +// aproj-add-app.cpp and aproj-add-lib.cpp are nearly identical while aproj-add-test.cpp has a few differences. If you change any +// of the three, make sure to keep the others similarly updated. + +#include +#include +#include +#include +#include + +#include + +#include + + + +std::string exe_name; +std::string indirect; + +int usage(std::string_view err) { + + std::ostream& os = (err.empty() ? std::cout : std::cerr); + + os << exe_name << ": PATH [APP_NAME [APP_LANG [APP_OPTIONS]]]\n" + << "\n" + << " --help Print this help and exit.\n" + << "\n" + << " PATH is either path to `project.yaml` or the path containing it.\n" + << " APP_NAME is the the name of the app to add.\n" + << " APP_LANG is the language of the additional app.\n" + << " APP_OPTIONS is the string of options to pass to the compiler.\n" + << "\n" + << " `project.yaml` is updated to add a new app.\n" + << "\n" + << " If either APP_NAME or APP_LANG is absent, the user is prompted.\n" + << "\n" + ; + + if(err.empty()) + return 0; + os << "Error: " << err << "\n"; + return -1; +} + + +int main(int argc, char** argv) { + + COMMON_INIT("Add an application entry to the project."); + + // Test arg count is valid. + if(argc < 2) + return usage("path is required."); + if(argc > 5) + return usage("too many options."); + + + // Get the path to the project. + std::filesystem::path path=argv[1]; + if(!antler::project::project::update_path(path)) + return usage("path either did not exist or no `project.yaml` file could be found."); + + // Load the project. + auto optional_proj = antler::project::project::parse(path); + if( !optional_proj ) + return usage("Failed to load project file."); + auto proj = optional_proj.value(); + + + std::string name; + antler::project::language lang=antler::project::language::none; + std::string opts; + + if(argc >= 3) { + name = argv[2]; + if(proj.object_exists(name, antler::project::object::type_t::app)) + return usage("APP_NAME already exists in project."); + } + + if(argc == 5) + opts = argv[3]; + + if(argc >= 4) + lang = antler::project::to_language(argv[3]); + else { + for(;;) { + if(!name.empty() && lang != antler::project::language::none) { + + std::cout + << "\n" + << "app name: " << name << "\n" + << "language: " << lang << "\n" + << "options: " << opts << "\n" + << "\n" + ; + + if(proj.object_exists(name, antler::project::object::type_t::app)) { + std::cerr << "Application " << name << " already exists in project. Can't add.\n\n"; + } + else { + if(proj.object_exists(name)) + std::cerr << "WARNING: " << name << " already exists in project as lib and/or test.\n\n"; + + if(is_this_correct()) + break; + } + } + + get_name("application name", name); + + for(;;) { + std::cout << "Enter project language: [" << lang << "]" << std::flush; + std::string temp; + std::getline(std::cin,temp); + if(temp.empty() && lang != antler::project::language::none) + break; + antler::project::language l2 = antler::project::to_language(temp); + if(l2 != antler::project::language::none) { + lang = l2; + break; + } + } + + { + std::cout << "Enter application options (space to clear): [" << opts << "]" << std::flush; + std::string temp; + std::getline(std::cin,temp); + if(temp == " ") + opts.clear(); + else if(!temp.empty()) + opts = temp; + } + + } + } + + + if(lang == antler::project::language::none) + return usage("invalid language."); + auto obj = antler::project::object(antler::project::object::app, name, lang, opts); + proj.upsert_app(std::move(obj)); + proj.sync(); + + return 0; +} diff --git a/aproj/aproj-add-dep.cpp b/aproj/aproj-add-dep.cpp new file mode 100644 index 0000000..e62c470 --- /dev/null +++ b/aproj/aproj-add-dep.cpp @@ -0,0 +1,201 @@ +#include +#include +#include +#include +#include + +#include + +#include + +std::string exe_name; +std::string indirect; + +int usage(std::string_view err) { + + std::ostream& os = (err.empty() ? std::cout : std::cerr); + + os << exe_name << ": PATH [OBJECT_NAME DEP_NAME [LOCATION [options]]]\n" + << "\n" + << " PATH is either path to `project.yaml` or the path containing it.\n" + << " OBJ_NAME is the the name of the object to receive DEP_NAME.\n" + << " DEP_NAME is the the name of this dependency.\n" + << " LOCATION is either a path or URL for finding this dependency.\n" + << "\n" + << " Options:\n" + << " --tag The github tag or commit hash; only valid when LOCATION is a github repository.\n" + << " --rel The github version for LOCATION.\n" + << " --hash SHA256 hash; only valid when LOCATION gets an archive (i.e. *.tar.gz or similar).\n" + << " --help Print this help and exit.\n" + << "\n" + << " The `project.yaml` object is updated to add a new dependency.\n" + << "\n" + << " If either OBJECT_NAME or DEP_NAME is absent, the user is prompted.\n" + << "\n" + ; + + if(err.empty()) + return 0; + os << "Error: " << err << "\n"; + return -1; +} + + +int main(int argc, char** argv) { + + COMMON_INIT("Add a dependency."); + + // Test arg count is valid. + if(argc < 2) + return usage("path is required."); + if(argc > 9) + return usage("too many options."); + + // Get the path to the project. + std::filesystem::path path=argv[1]; + if(!antler::project::project::update_path(path)) + return usage("path either did not exist or no `project.yaml` file could be found."); + + // Load the project. + auto optional_proj = antler::project::project::parse(path); + if( !optional_proj ) + return usage("Failed to load project file."); + auto proj = optional_proj.value(); + + + std::string obj_name; + std::string dep_name; + std::string dep_loc; + std::string dep_tag; + std::string dep_rel; + std::string dep_hash; + + + if(argc >= 3) { + obj_name = argv[2]; + if(!proj.object_exists(obj_name)) + return usage("OBJ_NAME does not exist in project."); + } + + if(argc >= 4) + dep_name = argv[3]; + + if(argc >= 5) + dep_loc = argv[4]; + + if(argc >= 6) { + for(int i=5; i < argc; ++i) { + std::string_view temp=argv[i]; + std::string_view next; + if(i+1 < argc) + next = argv[i+1]; + if(temp == "--tag") { + if(next.empty()) + return usage("--tag requires an argument."); + ++i; + dep_tag = next; + } + if(temp == "--rel") { + if(next.empty()) + return usage("--tag requires an argument."); + ++i; + dep_rel = next; + } + if(temp == "--hash") { + if(next.empty()) + return usage("--tag requires an argument."); + ++i; + dep_hash = next; + } + } + } + + // Assuming interactive mode. + const bool interactive = dep_name.empty(); + if(interactive) { + for(;;) { + if(!obj_name.empty() && !dep_name.empty() && antler::project::dependency::validate_location(dep_loc,dep_tag,dep_rel,dep_hash)) { + // Get the object to operate on. + auto obj_opt = proj.object(obj_name); + + // If it doesn't exist, none of the existing values can be correct, so alert and jump straigt to queries. + if(!obj_opt) + std::cerr << obj_name << " does not exist in project.\n"; + else { + std::cout + << "\n" + << "Object name (to update): " << obj_name << "\n" + << "Dependency name: " << dep_name << "\n" + << "Dependency location: " << dep_loc << "\n" + << "tag/commit hash: " << dep_tag << "\n" + << "release version: " << dep_rel << "\n" + << "SHA256 hash: " << dep_hash << "\n" + << "\n" + ; + + // Get object here and warn user if dep_name already exists. + auto obj = obj_opt.value(); + if(!dep_name.empty() && obj.dependency_exists(dep_name)) + std::cerr << dep_name << " already exists for " << obj_name << " in project.\n"; + + if(is_this_correct()) + break; + } + } + + // here we want to test that object name exists before we go on. + std::optional obj_opt; + for(;;) { + get_name("object (app/lib/test) name", obj_name); + obj_opt = proj.object(obj_name); + if(!obj_opt) { + std::cerr << obj_name << " does not exist in " << proj.name() << "\n"; + continue; + } + break; + } + + // here we want to validate dep name before we go on. + for(;;) { + get_name("dependency name", dep_name); + auto obj = obj_opt.value(); + if(!dep_name.empty() && obj.dependency_exists(dep_name)) { + std::cerr << dep_name << " already exists for " << obj_name << " in project.\n"; + continue; + } + break; + } + + get_loc ("from/location", dep_loc, true); + get_name("git tag/commit hash", dep_tag, true); + get_name("git release version", dep_rel, true); + get_hash("SHA-256 hash", dep_hash, true); + } + } + + // Get the object to update. + auto obj_opt = proj.object(obj_name); + if(!obj_opt) + RETURN_USAGE( << obj_name << " does not exist in project."); + auto obj = obj_opt.value(); + + // If we are not in interactive mode, test for the pre-existence of the dependency. + if(!interactive && obj.dependency_exists(dep_name)) + RETURN_USAGE(<< dep_name << " already exists for " << obj_name << " in project."); + + // Validate the location. Redundant for interactive mode, but cheap in human time. + std::ostringstream ss; + if(!antler::project::dependency::validate_location(dep_loc,dep_tag,dep_rel,dep_hash,ss)) + return usage(ss.str()); + + // Create the dependency, store it in the object, and store the object in the roject. + antler::project::dependency dep; + dep.set(dep_name, dep_loc, dep_tag, dep_rel, dep_hash); + obj.upsert_dependency(std::move(dep)); + proj.upsert(std::move(obj)); + + // Sync the project to storage. + if( !proj.sync() ) + return usage("failed to write project file."); + return 0; +} diff --git a/aproj/aproj-add-lib.cpp b/aproj/aproj-add-lib.cpp new file mode 100644 index 0000000..d056bc2 --- /dev/null +++ b/aproj/aproj-add-lib.cpp @@ -0,0 +1,140 @@ +// aproj-add-app.cpp and aproj-add-lib.cpp are nearly identical while aproj-add-test.cpp has a few differences. If you change any +// of the three, make sure to keep the others similarly updated. + +#include +#include +#include +#include +#include + +#include + +#include + + + +std::string exe_name; +std::string indirect; + +int usage(std::string_view err) { + + std::ostream& os = (err.empty() ? std::cout : std::cerr); + + os << exe_name << ": PATH [LIB_NAME [LIB_LANG [LIB_OPTIONS]]]\n" + << "\n" + << " PATH is either path to `project.yaml` or the path containing it.\n" + << " LIB_NAME is the the name of the lib to add.\n" + << " LIB_LANG is the language of the additional lib.\n" + << " LIB_OPTIONS is the string of options to pass to the compiler.\n" + << "\n" + << " `project.yaml` is updated to add a new lib.\n" + << "\n" + << " If either LIB_NAME or LIB_LANG is absent, the user is prompted.\n" + << "\n" + ; + + if(err.empty()) + return 0; + os << "Error: " << err << "\n"; + return -1; +} + + +int main(int argc, char** argv) { + + COMMON_INIT("Add a library entry."); + + // Test arg count is valid. + if(argc < 2) + return usage("path is required."); + if(argc > 5) + return usage("too many options."); + + // Get the path to the project. + std::filesystem::path path=argv[1]; + if(!antler::project::project::update_path(path)) + return usage("path either did not exist or no `project.yaml` file could be found."); + + // Load the project. + auto optional_proj = antler::project::project::parse(path); + if( !optional_proj ) + return usage("Failed to load project file."); + auto proj = optional_proj.value(); + + + std::string name; + antler::project::language lang=antler::project::language::none; + std::string opts; + + if(argc >= 3) { + name = argv[2]; + if(proj.object_exists(name, antler::project::object::type_t::lib)) + return usage("LIB_NAME already exists in project."); + } + + if(argc == 5) + opts = argv[3]; + + if(argc >= 4) + lang = antler::project::to_language(argv[3]); + else { + for(;;) { + if(!name.empty() && lang != antler::project::language::none) { + + std::cout + << "\n" + << "lib name: " << name << "\n" + << "language: " << lang << "\n" + << "options: " << opts << "\n" + << "\n" + ; + + if(proj.object_exists(name, antler::project::object::type_t::lib)) { + std::cerr << "Library " << name << " already exists in project. Can't add.\n\n"; + } + else { + if(proj.object_exists(name)) + std::cerr << "WARNING: " << name << " already exists in project as app and/or test.\n\n"; + + if(is_this_correct()) + break; + } + } + + get_name("library name", name); + + for(;;) { + std::cout << "Enter project language: [" << lang << "]" << std::flush; + std::string temp; + std::getline(std::cin,temp); + if(temp.empty() && lang != antler::project::language::none) + break; + antler::project::language l2 = antler::project::to_language(temp); + if(l2 != antler::project::language::none) { + lang = l2; + break; + } + } + + { + std::cout << "Enter library options (space to clear): [" << opts << "]" << std::flush; + std::string temp; + std::getline(std::cin,temp); + if(temp == " ") + opts.clear(); + else if(!temp.empty()) + opts = temp; + } + + } + } + + + if(lang == antler::project::language::none) + return usage("invalid language."); + auto obj = antler::project::object(antler::project::object::lib, name, lang, opts); + proj.upsert_lib(std::move(obj)); + proj.sync(); + + return 0; +} diff --git a/aproj/aproj-add-test.cpp b/aproj/aproj-add-test.cpp new file mode 100644 index 0000000..c5ad23a --- /dev/null +++ b/aproj/aproj-add-test.cpp @@ -0,0 +1,122 @@ +// aproj-add-app.cpp and aproj-add-lib.cpp are nearly identical while aproj-add-test.cpp has a few differences. If you change any +// of the three, make sure to keep the others similarly updated. + +#include +#include +#include +#include +#include + +#include + +#include + + + +std::string exe_name; +std::string indirect; + +int usage(std::string_view err) { + + std::ostream& os = (err.empty() ? std::cout : std::cerr); + + os << exe_name << ": PATH [TEST_NAME [TEST_CMD]]\n" + << "\n" + << " PATH is either path to `project.yaml` or the path containing it.\n" + << " TEST_NAME is the the name of the test to add.\n" + << " TEST_CMD is the testing command to execute.\n" + << "\n" + << " `project.yaml` is updated to add a new test.\n" + << "\n" + << " If either TEST_NAME or TEST_CMD is absent, the user is prompted.\n" + << " Note that an empty value for TEST_CMD is valid (e.g. \"\" is a valid empty argument.\n" + << "\n" + ; + + if(err.empty()) + return 0; + os << "Error: " << err << "\n"; + return -1; +} + + +int main(int argc, char** argv) { + + COMMON_INIT("Add a test entry."); + + // Test arg count is valid. + if(argc < 2) + return usage("path is required."); + if(argc > 4) + return usage("too many options."); + + // Get the path to the project. + std::filesystem::path path=argv[1]; + if(!antler::project::project::update_path(path)) + return usage("path either did not exist or no `project.yaml` file could be found."); + + // Load the project. + auto optional_proj = antler::project::project::parse(path); + if( !optional_proj ) + return usage("Failed to load project file."); + auto proj = optional_proj.value(); + + + std::string name; + std::string cmd; + + if(argc >= 3) { + name = argv[2]; + if(proj.object_exists(name, antler::project::object::type_t::test)) + return usage("TEST_NAME already exists in project."); + } + + if(argc >= 4) + cmd = argv[3]; + else { + for(;;) { + if(!name.empty()) { + + std::cout + << "\n" + << "test name: " << name << "\n" + << "command: " << cmd << "\n" + << "\n" + ; + + if(proj.object_exists(name, antler::project::object::type_t::test)) { + std::cerr << "Test " << name << " already exists in project. Can't add.\n\n"; + } + else { + if(proj.object_exists(name)) + std::cerr << "WARNING: " << name << " already exists in project as app and/or lib.\n\n"; + + if(is_this_correct()) + break; + } + } + + get_name("test name", name); + + for(;;) { + std::cout << "Enter test command (space to clear): [" << cmd << "]" << std::flush; + std::string temp; + std::getline(std::cin,temp); + if(temp == " ") + cmd.clear(); + else if(!temp.empty()) + cmd = temp; + else + continue; + break; + } + } + } + + + auto obj = antler::project::object(name, cmd); + proj.upsert_test(std::move(obj)); + proj.sync(); + + return 0; +} diff --git a/aproj/aproj-common.h b/aproj/aproj-common.h new file mode 100644 index 0000000..852512d --- /dev/null +++ b/aproj/aproj-common.h @@ -0,0 +1,227 @@ +#ifndef antler_aproj_aproj_common_h +#define antler_aproj_aproj_common_h + +#include +#include +#include + +#include +#include + +const std::string_view project_prefix{"aproj-"}; + +inline void print_brief(std::string& exe_name, const std::string& brief_text) { + exe_name.erase(0,project_prefix.size()); + //constexpr size_t width{17}; + //const size_t space_count = (exe_name.size() < width ? width-exe_name.size() : 2); + //exe_name.append(space_count,' '); + std::cout << "--" << exe_name << ' ' << brief_text << '\n'; +} + + +#define RETURN_USAGE(X) { std::stringstream ss; ss X; return usage(ss.str()); } + +#define COMMON_INIT(BRIEF_TEXT) { \ + /* set exe name for usage */ \ + exe_name = std::filesystem::path(argv[0]).filename().string(); \ + /* search for indirect string */ \ + if(argc > 0) { \ + constexpr std::string_view indirect_str{"--indirect="}; \ + if(std::string_view(argv[argc-1]).starts_with(indirect_str)) { \ + indirect = std::string_view(argv[argc-1]).substr(indirect_str.size()); \ + exe_name = indirect; \ + --argc; \ + } \ + } \ + /* search for brief and help flags */ \ + for(int i=0; i < argc; ++i) { \ + if(std::string_view(argv[i]) == "--help") \ + return usage(""); \ + if(std::string_view(argv[i]) == "--brief") { \ + if(!exe_name.starts_with(project_prefix)) { \ + std::cerr << "Can't provide --brief for" << argv[0] << '\n'; \ + return -1; \ + } \ + print_brief(exe_name,BRIEF_TEXT); \ + return 0; \ + } \ + } \ + } // COMMON_INIT + + + +template +inline void dump_obj_deps(const T& obj_list, bool app, bool lib, bool tst, std::ostream& os=std::cout) { + os << "Displaying dependencies from entries of type:"; + if(app) + os << " app"; + if(lib) + os << " lib"; + if(tst) + os << " test"; + os << "\n"; + + for(const auto& a : obj_list) { + const auto& list = a.dependencies(); + if(list.empty()) + continue; + switch(a.type()) { + case antler::project::object::type_t::app: if(!app) { continue; } break; + case antler::project::object::type_t::lib: if(!lib) { continue; } break; + case antler::project::object::type_t::test: if(!tst) { continue; } break; + case antler::project::object::type_t::none: + case antler::project::object::type_t::any: + std::cerr << "Unexpected type: " << a.type() << " in object: " << a.name() << "\n"; + continue; + } + + auto i = list.begin(); + os << " " << a.name() << " [" << a.type() << "]: " << i->name(); + for(++i; i != list.end(); ++i) + os << ", " << i->name(); + os << "\n"; + + } +} + + +template +inline void dump_obj_deps(const T& obj_list, std::ostream& os=std::cout) { + dump_obj_deps(obj_list, true, true, true, os); +} + + +/// ask the user if this is correct. +inline bool is_this_correct(std::string_view msg="Is this correct?") noexcept { + std::string yn="x"; // yes or no? + while(yn != "y" && yn != "n") { + std::cout << msg << " [Y/n]" << std::flush; + std::getline(std::cin,yn); + for(auto& a : yn) + a = static_cast(tolower(a)); + if(yn.empty()) + yn = "y"; + } + return yn == "y"; +} + + + +/// Test to see if an object (app/lib/test) name is valid. +inline bool is_valid_name(std::string_view s) noexcept { + if(s.empty()) + return false; + for(auto a : s) { + if( !isalnum(a) && a != '_') // && a != '-' + return false; + } + return true; +} + + + +inline void get_name(std::string_view friendly_name, std::string& name, bool allow_empty=false) noexcept { + // Loop until return. + for(;;) { + std::cout << "Enter " << friendly_name; + if(allow_empty) + std::cout << " (space to clear)"; + std::cout << ": [" << name << "]" << std::flush; + + std::string temp; + std::getline(std::cin,temp); + // No change? + if(temp.empty()) { + if(allow_empty || !name.empty()) + return; + } + else { + if(allow_empty && temp == " ") { + name.clear(); + return; + } + if(is_valid_name(temp)) { + name = temp; + return; + } + } + } +} + + +/// Test to see if a hash is valid. +inline bool is_valid_hash(std::string_view s, size_t byte_count=32) noexcept { + if(s.size() != byte_count) + return false; + for(auto a : s) { + if( !(a >= '0' && a <= '9') + && !(a >= 'a' && a <= 'f') + && !(a >= 'A' && a <= 'F') ) { + return false; + } + } + return true; +} + + +inline void get_hash(std::string_view friendly_name, std::string& hash, bool allow_empty=false) noexcept { + // Loop until return. + for(;;) { + std::cout << "Enter " << friendly_name; + if(allow_empty) + std::cout << " (space to clear)"; + std::cout << ": [" << hash << "]" << std::flush; + + std::string temp; + std::getline(std::cin,temp); + // No change? + if(temp.empty()) { + if(allow_empty || !hash.empty()) + return; + } + else { + if(allow_empty && temp == " ") { + hash.clear(); + return; + } + if(is_valid_hash(temp)) { + hash = temp; + return; + } + } + } +} + + +inline void get_loc(std::string_view friendly_name, std::string& loc, bool allow_empty=false) noexcept { + // Loop until return. + for(;;) { + std::cout << "Enter " << friendly_name; + if(allow_empty) + std::cout << " (space to clear)"; + std::cout << ": [" << loc << "]" << std::flush; + + std::string temp; + std::getline(std::cin,temp); + // No change? + if(temp.empty()) { + if(allow_empty || !loc.empty()) + return; + } + else { + if(allow_empty && temp == " ") { + loc.clear(); + return; + } + if(antler::project::dependency::validate_location(temp)) { + loc = temp; + return; + } + } + } +} + + + + +#endif diff --git a/aproj/aproj-init.cpp b/aproj/aproj-init.cpp new file mode 100644 index 0000000..3bcd08a --- /dev/null +++ b/aproj/aproj-init.cpp @@ -0,0 +1,155 @@ +#include +#include +#include +#include +#include + +#include +#include + + + +std::string exe_name; +std::string indirect; + +int usage(std::string_view err) { + + std::ostream& os = (err.empty() ? std::cout : std::cerr); + + os << exe_name << ": PATH [PROJECT_NAME [VERSION]]\n" + << "\n" + << " PATH is the root path to create the project in.\n" + << " PROJECT_NAME is the the name of the project.\n" + << " VERSION is the version to store in the project file.\n" + << "\n" + << " `project.yaml` is created in PATH if PATH is an empty directory AND the filename matches PROJECT_NAME;\n" + << " otherwise, a directory matching PROJECT_NAME is created at PATH to contain `project.yaml`.\n" + << "\n" + << " If PROJECT_NAME is absent, the user is prompted.\n" + << " If PROJECT_NAME exists, VERSION will default to 0.0.0\n" + << "\n" + ; + + if(err.empty()) + return 0; + os << "Error: " << err << "\n"; + return -1; +} + + +int main(int argc, char** argv) { + + COMMON_INIT("Initialize a new projet creating the directory tree and a `project.yaml` file."); + + if(argc < 2) + return usage("path is required."); + if(argc > 4) + return usage("too many options."); + + std::error_code sec; + + std::string name; + + // Sanity check potential project directory. + auto in_path = std::filesystem::path(argv[1]); + if(!std::filesystem::exists(in_path,sec)) + name = in_path.filename().string(); + else { + // It might be okay if it exists, but only if it's a directory AND it's empty. + if(!std::filesystem::is_directory(in_path,sec)) + return usage(in_path.string() + " already exists."); + if(!std::filesystem::is_empty(in_path,sec)) { + if(std::filesystem::exists(in_path/"project.yaml")) + return usage("not initializing where a `project.yaml` file already exists."); + } + else if(!in_path.has_extension()) + name = in_path.filename().string(); + } + + antler::project::version ver("0.0.0"); + + if(argc > 2) + name = argv[2]; + if(argc > 3) + ver = argv[3]; + + auto project_root = in_path; + if(in_path.filename() != name) + project_root /= name; + + if(argc == 2) { + for(;;) { + if(!name.empty()) { + + project_root = in_path; + if(in_path.filename() != name) + project_root /= name; + + std::cout + << "\n" + << "Path: " << project_root << "\n" + << "Project name: " << name << "\n" + << "Version: " << ver << "\n" + << "\n" + ; + + if(is_this_correct()) + break; + } + + for(;;) { + std::cout << "Enter project name: [" << name << "]" << std::flush; + std::string temp; + std::getline(std::cin,temp); + if(is_valid_name(temp)) + name = temp; + if(!name.empty()) + break; + } + + { + std::cout << "Enter project version: [" << ver << "]" << std::flush; + std::string temp; + std::getline(std::cin,temp); + if(!temp.empty()) + ver = temp; + } + } + } + + + if(!is_valid_name(name)) + RETURN_USAGE( << "name \"" << name << "\" contains invalid chars. Expecting [0-9a-zA-Z_]." ); + + + // Do initialization here: + + // Create the root directory. + std::filesystem::create_directories(project_root,sec); + if(sec) + RETURN_USAGE( << project_root << " could not be created: " << sec); + + + if(!std::filesystem::is_empty(project_root,sec)) + RETURN_USAGE( << project_root << " is NOT empty!" ); + + // Create the directory structure. + { + const std::vector files = {"apps", "include", "ricardian", "libs", "tests" }; + for(const auto& fn : files) { + std::filesystem::create_directory(project_root/fn,sec); + if(sec) + RETURN_USAGE( << (project_root/fn) << " could not be created: " << sec); + } + } + + // Create an empty project and populate it. + antler::project::project proj; + proj.path(project_root/"project.yaml"); + proj.name(name); + proj.version(ver); + proj.sync(); + + + return -1; +} diff --git a/aproj/aproj-populate.cpp b/aproj/aproj-populate.cpp new file mode 100644 index 0000000..88e64a9 --- /dev/null +++ b/aproj/aproj-populate.cpp @@ -0,0 +1,70 @@ +#include +#include + +#include + +#include + + + +std::string exe_name; +std::string indirect; + + +int usage(std::string_view err) { + + std::ostream& os = (err.empty() ? std::cout : std::cerr); + + os << exe_name << ": PATH [APP_NAME [APP_LANG [APP_OPTIONS]]]\n" + << "\n" + << " --help Print this help and exit.\n" + << "\n" + << " PATH is either path to `project.yaml` or the path containing it.\n" + << " APP_NAME is the the name of the app to add.\n" + << " APP_LANG is the language of the additional app.\n" + << " APP_OPTIONS is the string of options to pass to the compiler.\n" + << "\n" + << " `project.yaml` is updated to add a new app.\n" + << "\n" + << " If either APP_NAME or APP_LANG is absent, the user is prompted.\n" + << "\n" + ; + + if(err.empty()) + return 0; + os << "Error: " << err << "\n"; + return -1; +} + + +int main(int argc, char** argv) { + + COMMON_INIT("Populate the project with CMake files."); + + // Test arg count is valid. + if(argc < 2) + return usage("path is required."); + if(argc > 2) + return usage("too many options."); + + + // Get the path to the project. + std::filesystem::path path=argv[1]; + if(!antler::project::project::update_path(path)) + return usage("path either did not exist or no `project.yaml` file could be found."); + + // Load the project. + auto optional_proj = antler::project::project::parse(path); + if( !optional_proj ) + return usage("Failed to load project file."); + auto proj = optional_proj.value(); + + auto pop_type = antler::project::project::pop::honor_deltas; + + bool result = proj.populate(pop_type); + + if(result) + return 0; + std::cerr << "Fail\n"; + return -1; +} diff --git a/aproj/aproj-rm-dep.cpp b/aproj/aproj-rm-dep.cpp new file mode 100644 index 0000000..24676ec --- /dev/null +++ b/aproj/aproj-rm-dep.cpp @@ -0,0 +1,267 @@ +#include +#include +#include +#include +#include + +#include + +#include + + +std::string exe_name; +std::string indirect; + +int usage(std::string_view err) { + + std::ostream& os = (err.empty() ? std::cout : std::cerr); + + os << exe_name << ": PATH [DEP_NAME] [OPTIONS]\n" + << "\n" + << "Remove dependency from project applications, libraries, and tests.\n" + << "\n" + << " PATH is either path to `project.yaml` or the path containing it.\n" + << " DEP_NAME is the the name of the dependency to remove.\n" + << "\n" + << " Options:\n" + << " --all Remove the dep from all objects (implies --app, --lib, test). Default option.\n" + << " --app Remove the dep from application objects.\n" + << " --lib Remove the dep from library objects.\n" + << " --test Remove the dep from test objects.\n" + << " --name=OBJ_NAME Remove the dep from object OBJ_NAME.\n" + << "\n" + << " The `project.yaml` object is updated to add a new dependency.\n" + << "\n" + << " If DEP_NAME is absent, the user is prompted.\n" + << "\n" + ; + + if(err.empty()) + return 0; + os << "Error: " << err << "\n"; + return -1; +} + + +template +std::string to_string(T list) { + auto pos = list.begin(); + auto end = list.end(); + std::ostringstream ss; + ss << "{ "; + if(pos != end) { + ss << *pos; + for(++pos; pos != end; ++pos) + ss << ", " << *pos; + } + ss << " }"; + return ss.str(); +} + + +antler::project::object::list_t populate_update_list( + const antler::project::object::list_t& src, ///< The list of objects. + std::string_view dep_name, ///< The dependency to remove. + std::string_view obj_name, ///< The name of the object (or empty) to remove from. + bool app, bool lib, bool tst) ///< Valid object types to remove from. + noexcept { + + antler::project::object::list_t rv; + for(const auto& a : src) { + switch(a.type()) { + case antler::project::object::type_t::app: if(!app) { continue; } break; + case antler::project::object::type_t::lib: if(!lib) { continue; } break; + case antler::project::object::type_t::test: if(!tst) { continue; } break; + case antler::project::object::type_t::none: + case antler::project::object::type_t::any: + std::cerr << "Unexpected type: " << a.type() << " in object: " << a.name() << "\n"; + continue; + } + + if( (obj_name.empty() || a.name() == obj_name) && a.dependency_exists(dep_name) ) { + rv.emplace_back(a); + rv.back().remove_dependency(dep_name); + } + } + + return rv; +} + + +int main(int argc, char** argv) { + + COMMON_INIT("Remove a dependency."); + + // Test arg count is valid. + if(argc < 2) + return usage("path is required."); + + // Get the path to the project. + std::filesystem::path path=argv[1]; + if(!antler::project::project::update_path(path)) + return usage("path either did not exist or no `project.yaml` file could be found."); + + // Load the project. + auto optional_proj = antler::project::project::parse(path); + if( !optional_proj ) + return usage("Failed to load project file."); + auto proj = optional_proj.value(); + + // Get all the objects and their names. + const auto all_objs = proj.all_objects(); + + + // Find all the object and dependency names. + std::vector all_obj_names; + std::vector all_dep_names; + for(const auto& a : all_objs) { + all_obj_names.emplace_back(a.name()); + for(const auto& b : a.dependencies()) + all_dep_names.emplace_back(b.name()); + } + + // Sort dep name list and make it unique. + std::sort(all_dep_names.begin(), all_dep_names.end()); + all_dep_names.erase( std::unique(all_dep_names.begin(), all_dep_names.end()), all_dep_names.end()); + // If there aren't any deps, then there's nothing to do... + if(all_dep_names.empty()) + return usage("project file does not contain any dependencies."); + + + // Sort obj name list and make it unique. + std::sort(all_obj_names.begin(), all_obj_names.end()); + all_obj_names.erase( std::unique(all_obj_names.begin(), all_obj_names.end()), all_obj_names.end()); + + + std::string dep_name; + bool rm_all = false; + bool rm_app = false; + bool rm_lib = false; + bool rm_test = false; + std::string obj_name; + + for(int i = 2; i < argc; ++i) { + std::string_view temp = argv[i]; + + if(temp == "--all") { + rm_all = true; + if(rm_lib || rm_app || rm_test) + return usage("--all conflicts with the flags --app, --lib, and --test."); + continue; + } + + if(temp == "--app") { + rm_app = true; + if(rm_all) + return usage("--all conflicts with the flags --app, --lib, and --test."); + continue; + } + + if(temp == "--lib") { + rm_lib = true; + if(rm_all) + return usage("--all conflicts with the flags --app, --lib, and --test."); + continue; + } + + if(temp == "--test") { + rm_test = true; + if(rm_all) + return usage("--all conflicts with the flags --app, --lib, and --test."); + continue; + } + + if(temp.starts_with("--name=")) { + obj_name = temp.substr(7); + if(obj_name.empty()) + return usage("--name=OBJ_NAME argument requires a value."); + continue; + } + + // All the flags have been looked at, all we have left is a single dependency name. + if(!dep_name.empty()) + return usage("Received multiple dependency names, only one is allowed."); + dep_name = temp; + } + + // Set the individual rm flags if all was explicitly set OR implicitly (i.e. no rm flags at all). + if(rm_all || (!rm_app && !rm_lib && !rm_test)) { + rm_app = true; + rm_lib = true; + rm_test = true; + } + + + antler::project::object::list_t update_list; + + // An existing dep_name indicates a non--interactive mode. + if(!dep_name.empty()) { + // So just update the list. + update_list = populate_update_list(all_objs, dep_name, obj_name, rm_app, rm_lib, rm_test); + } + else { + // Get input from the user. + bool first_time = true; + antler::project::object::list_t temp0 = populate_update_list(all_objs, dep_name, obj_name, rm_app, rm_lib, rm_test); + + for(;;) { + + if(temp0.empty()) { + dump_obj_deps(all_objs, rm_app, rm_lib, rm_test); + if(!first_time) + std::cerr << "No objects with dependency " << dep_name << "\n\n"; + } + else { + std::cout + << "\n" + << "Dependency name: " << dep_name << "\n" + ; + if(!obj_name.empty()) + std::cout << "Object name: " << obj_name << "\n"; + + std::cout << "Objects that will be updated (" << temp0.size() << "):\n"; + for(auto a : temp0) + std::cout << " " << a.name() << " [" << a.type() << "]\n"; + std::cout << "\n"; + + if(is_this_correct()) { + update_list = temp0; + break; + } + } + + first_time = false; + + // dependency name + for(;;) { + get_name("dependency name", dep_name); + if(std::find(all_dep_names.begin(),all_dep_names.end(),dep_name) == all_dep_names.end()) { + std::cerr << "Valid dependencies: \n"; + for(auto a : all_dep_names) + std::cout << " " << a << "\n"; + continue; + } + break; + } + + // object name + get_name("object name", obj_name, true); + + // Update the temporary removal list. + temp0 = populate_update_list(all_objs, dep_name, obj_name, rm_app, rm_lib, rm_test); + } + } + + // Sanity check. This might be better if it returned true in certain circumstances? + if(update_list.empty()) + return usage("no objects were selected for removal."); + + // Update the objects that need updating. + for(auto a : update_list) + proj.upsert(std::move(a)); + + // Sync the project to storage. + if( !proj.sync() ) + return usage("failed to write project file."); + return 0; +} diff --git a/aproj/aproj-update-dep.cpp b/aproj/aproj-update-dep.cpp new file mode 100644 index 0000000..61bf25c --- /dev/null +++ b/aproj/aproj-update-dep.cpp @@ -0,0 +1,249 @@ +#include +#include +#include +#include +#include + +#include + +#include + +std::string exe_name; +std::string indirect; + +int usage(std::string_view err) { + + std::ostream& os = (err.empty() ? std::cout : std::cerr); + + os << exe_name << ": PATH [OBJECT_NAME DEP_NAME [LOCATION [options]]]\n" + << "\n" + << " PATH is either path to `project.yaml` or the path containing it.\n" + << " OBJ_NAME is the the name of the object to receive DEP_NAME.\n" + << " DEP_NAME is the the name of this dependency.\n" + << " LOCATION is either a path or URL for finding this dependency.\n" + << "\n" + << " Options:\n" + << " --tag The github tag or commit hash; only valid when LOCATION is a github repository.\n" + << " --rel The github version for LOCATION.\n" + << " --hash SHA256 hash; only valid when LOCATION gets an archive (i.e. *.tar.gz or similar).\n" + << " --help Print this help and exit.\n" + << "\n" + << " The `project.yaml` object's dependency DEP_NAME is updated with the values.\n" + << "\n" + << " If either OBJECT_NAME or DEP_NAME is absent, the user is prompted.\n" + << "\n" + ; + + if(err.empty()) + return 0; + os << "Error: " << err << "\n"; + return -1; +} + + +int main(int argc, char** argv) { + + COMMON_INIT("Update a dependency."); + + // Test arg count is valid. + if(argc < 2) + return usage("path is required."); + if(argc > 9) + return usage("too many options."); + + // Get the path to the project. + std::filesystem::path path=argv[1]; + if(!antler::project::project::update_path(path)) + return usage("path either did not exist or no `project.yaml` file could be found."); + + // Load the project. + auto optional_proj = antler::project::project::parse(path); + if( !optional_proj ) + return usage("Failed to load project file."); + auto proj = optional_proj.value(); + + // Get all the objects and their names. + const auto all_objs = proj.all_objects(); + + + std::string obj_name; + std::string dep_name; + std::string dep_loc; + std::string dep_tag; + std::string dep_rel; + std::string dep_hash; + + + if(argc >= 3) { + obj_name = argv[2]; + if(!proj.object_exists(obj_name)) + return usage("OBJ_NAME does not exist in project."); + } + + if(argc >= 4) + dep_name = argv[3]; + + if(argc >= 5) + dep_loc = argv[4]; + + if(argc >= 6) { + for(int i=5; i < argc; ++i) { + std::string_view temp=argv[i]; + std::string_view next; + if(i+1 < argc) + next = argv[i+1]; + if(temp == "--tag") { + if(next.empty()) + return usage("--tag requires an argument."); + ++i; + dep_tag = next; + } + if(temp == "--rel") { + if(next.empty()) + return usage("--tag requires an argument."); + ++i; + dep_rel = next; + } + if(temp == "--hash") { + if(next.empty()) + return usage("--tag requires an argument."); + ++i; + dep_hash = next; + } + } + } + + // Assuming interactive mode. + const bool interactive = dep_name.empty(); + if(interactive) { + bool first_time = true; + + for(;;) { + + if(first_time) { + + if(argc == 4) { // We have exactly obj and dep names. + // try to get object then try to get dep. + auto obj_opt = proj.object(obj_name); + if(obj_opt) { + auto dep_opt = obj_opt->dependency(dep_name); + if(dep_opt) { + auto dep = *dep_opt; + dep_loc = dep.location(); + dep_tag = dep.tag(); + dep_rel = dep.release(); + dep_hash = dep.hash(); + } + } + } + } + + if(obj_name.empty() || dep_name.empty()) { + dump_obj_deps(all_objs); + } + else if(!obj_name.empty() && !dep_name.empty() && antler::project::dependency::validate_location(dep_loc,dep_tag,dep_rel,dep_hash)) { + // Get the object to operate on. + auto obj_opt = proj.object(obj_name); + + // If it doesn't exist, none of the existing values can be correct, so alert and jump straigt to queries. + if(!obj_opt) + std::cerr << obj_name << " does not exist in project.\n"; + else { + std::cout + << "\n" + << "Object name (to update): " << obj_name << "\n" + << "Dependency name: " << dep_name << "\n" + << "Dependency location: " << dep_loc << "\n" + << "tag/commit hash: " << dep_tag << "\n" + << "release version: " << dep_rel << "\n" + << "SHA256 hash: " << dep_hash << "\n" + << "\n" + ; + + // Get object here and warn user if dep_name des NOT exists. + auto obj = obj_opt.value(); + if(!dep_name.empty() && !obj.dependency_exists(dep_name)) + std::cerr << dep_name << " does not exists for " << obj_name << " in project.\n"; + + if(is_this_correct()) + break; + } + } + + auto old_obj_name = obj_name; + auto old_dep_name = dep_name; + + // here we want to test that object name exists before we go on. + std::optional obj_opt; + for(;;) { + get_name("object (app/lib/test) name", obj_name); + obj_opt = proj.object(obj_name); + if(!obj_opt) { + std::cerr << obj_name << " does not exist in " << proj.name() << "\n"; + continue; + } + break; + } + + // here we want to validate dep name before we go on. + for(;;) { + get_name("dependency name", dep_name); + auto obj = obj_opt.value(); + if(!dep_name.empty() && !obj.dependency_exists(dep_name)) { + std::cerr << dep_name << " does not exists for " << obj_name << " in project.\n"; + continue; + } + break; + } + + + // We should have obj and dep names, if they changed let's reload location etc. + if(old_obj_name != obj_name || old_dep_name != dep_name) { + // try to get object then try to get dep. + if(obj_opt) { + auto dep_opt = obj_opt->dependency(dep_name); + if(dep_opt) { + auto dep = *dep_opt; + dep_loc = dep.location(); + dep_tag = dep.tag(); + dep_rel = dep.release(); + dep_hash = dep.hash(); + } + } + } + + get_loc ("from/location", dep_loc, true); + get_name("git tag/commit hash", dep_tag, true); + get_name("git release version", dep_rel, true); + get_hash("SHA-256 hash", dep_hash, true); + + first_time = false; + } + } + + // Get the object to update. + auto obj_opt = proj.object(obj_name); + if(!obj_opt) + RETURN_USAGE( << obj_name << " does not exist in project."); + auto obj = obj_opt.value(); + + // If we are not in interactive mode, test for the pre-existence of the dependency. + if(!interactive && obj.dependency_exists(dep_name)) + RETURN_USAGE(<< dep_name << " already exists for " << obj_name << " in project."); + + // Validate the location. Redundant for interactive mode, but cheap in human time. + std::ostringstream ss; + if(!antler::project::dependency::validate_location(dep_loc,dep_tag,dep_rel,dep_hash,ss)) + return usage(ss.str()); + + // Create the dependency, store it in the object, and store the object in the roject. + antler::project::dependency dep; + dep.set(dep_name, dep_loc, dep_tag, dep_rel, dep_hash); + obj.upsert_dependency(std::move(dep)); + proj.upsert(std::move(obj)); + + // Sync the project to storage. + if( !proj.sync() ) + return usage("failed to write project file."); + return 0; +} diff --git a/aproj/aproj-validate.cpp b/aproj/aproj-validate.cpp new file mode 100644 index 0000000..1172202 --- /dev/null +++ b/aproj/aproj-validate.cpp @@ -0,0 +1,69 @@ +#include +#include +#include +#include + +std::string exe_name; +std::string indirect; + +int usage(std::string_view err) { + + std::ostream& os = (err.empty() ? std::cout : std::cerr); + + os << exe_name << ": PATH\n" + << "\n" + << " Attempt to load the project.yaml file pointed to by PATH.\n" + << "\n" + << " --help Print this help and exit.\n" + << " -q,--quiet Do NOT print the contents of the project.yaml file.\n" + << "\n" + << " PATH is either path to `project.yaml` or the path containing it.\n" + << "\n" + ; + + if(err.empty()) + return 0; + os << "Error: " << err << "\n"; + return -1; +} + + +int main(int argc, char** argv) { + + COMMON_INIT("Attempt to load a project.yaml file. This is the default command."); + + // Test arg count is valid. + if(argc < 2) + return usage("path is required."); + if(argc > 3) + return usage("too many options."); + + + // Get the path to the project. + std::filesystem::path path=argv[1]; + if(!antler::project::project::update_path(path)) + return usage("path either did not exist or no `project.yaml` file could be found."); + + bool quiet=false; + + for(int i=2; i < argc; ++i) { + std::string_view arg = argv[i]; + if( arg == "-q" || arg == "--quiet") { + quiet = true; + continue; + } + + RETURN_USAGE( << "argument " << arg << " not valid in this context."); + } + + // Load the project. + auto optional_proj = antler::project::project::parse(path); + if( !optional_proj ) + return usage("Failed to load project file."); + auto proj = optional_proj.value(); + + if(!quiet) + std::cout << proj.to_yaml() << "\n"; + + return 0; +} diff --git a/aproj/aproj.cpp b/aproj/aproj.cpp new file mode 100644 index 0000000..62c94b9 --- /dev/null +++ b/aproj/aproj.cpp @@ -0,0 +1,156 @@ +#include +#include +#include +#include +#include +#include // std::sort + +#include +#include +#include +#include + + +#include + + +namespace { // anonymous + +std::string exe_name; + +struct app_entry { + std::vector args; + std::filesystem::path path; + std::string arg_str; + std::string brief; +}; +std::vector apps; + + +int usage(std::string_view err="") { + + constexpr std::string_view help_arg{"--help"}; + + size_t width = help_arg.size(); + for(const auto& a : apps) { + width = std::max(width,a.arg_str.size()); + } + width += 3; + + std::sort(apps.begin(),apps.end(), [](const app_entry& l, const app_entry& r) { return l.arg_str < r.arg_str; }); + + std::ostream& os = (err.empty() ? std::cout : std::cerr); + + os << exe_name << ": COMMAND [options]\n" + << "\n" + << " Commands:\n" + ; + + for(const auto& a : apps) { + std::string pad(width-a.arg_str.size(),' '); + os << " " << a.arg_str << pad << a.brief << '\n'; + } + // add help: + { + std::string pad(width-help_arg.size(),' '); + os << " " << help_arg << pad << "Show this help and exit.\n"; + } + os << '\n' + << " Options vary by command and may be viewed with --help.\n" + ; + if(err.empty()) + return 0; + os << "Error: " << err << "\n"; + return -1; +} + + +template +int exec_helper(std::filesystem::path exe, iterator_type begin, iterator_type end, std::string_view cmd) { + + std::stringstream ss; + ss << exe; + for(auto i = begin; i < end; ++i) + ss << " " << *i; + ss << " --indirect=\"" << exe_name << ' ' << cmd << '"'; + + return system(ss.str().c_str()); +} + +} + + +int main(int argc, char** argv) { + + + std::filesystem::path bin_path = sb::filesystem::executable_path().parent_path(); + std::filesystem::path project_path = std::filesystem::current_path(); + + // Update globals - these are for the usage() function and in the arg list decoder. + exe_name = std::filesystem::path(argv[0]).filename().string(); + // Get the sub commands. + for (auto const& entry : std::filesystem::directory_iterator{bin_path}) { + const auto path = entry.path(); + if(!path.stem().string().starts_with(project_prefix)) + continue; + auto result = antler::system::exec(path.string() + " --brief"); + if(!result) { + std::cerr << "failed for " << path << '\n'; + } + else { + auto spc = result.output.find_first_of(' '); + if(spc != std::string::npos) { + app_entry ae; + ae.path = path; + ae.arg_str = result.output.substr(0,spc); + ae.args = antler::string::split(ae.arg_str,","); + ae.brief = result.output.substr(spc+1); + while(ae.brief.back() == '\n') + ae.brief.pop_back(); + apps.push_back(ae); + } + } + } + + if( argc < 2) + return usage(); + + std::vector args; + for(int i=1; i < argc; ++i) + args.push_back(argv[i]); + + for(auto i=args.begin(); i != args.end(); ++i) { + + if(*i == "help" || *i == "--help") + return usage(); + + for(const auto& a : apps) { + for(const auto& test_arg : a.args) { + if( *i == test_arg || (std::string{"--"}+std::string(*i) == test_arg)) + return exec_helper(a.path, ++i, args.end(), *i); + } + } + + if(*i == "add") { + if(++i == args.end()) + return usage("`add` requires sub command (e.g. `add lib`)."); + std::string cmd{"--add-"}; + cmd += *i; + + std::string real_cmd{"add "}; + real_cmd += *i; + + for(const auto& a : apps) { + for(const auto& test_arg : a.args) { + if( cmd == test_arg ) + return exec_helper(a.path, ++i, args.end(), real_cmd); + } + } + } + // return system_helper(bin_path, aproj_cmd::validate, project_path.string(), ++i, args.end(), indirect(*i)); + + return usage(std::string("Bad argument: ") + std::string(*i)); + } + + return usage("No command supplied."); +} diff --git a/common.cmake b/common.cmake new file mode 100644 index 0000000..3b9f330 --- /dev/null +++ b/common.cmake @@ -0,0 +1,129 @@ +# common.cmake +# +# This file sets the following values: +# +# +# CMAKE_RUNTIME_OUTPUT_DIRECTORY_${CMAKE_BUILD_TYPE} +# CMAKE_LIBRARY_OUTPUT_DIRECTORY_${CMAKE_BUILD_TYPE} +# CMAKE_ARCHIVE_OUTPUT_DIRECTORY_${CMAKE_BUILD_TYPE} +# +# CMAKE_DEBUG_POSTFIX +# +# CMAKE_C_FLAGS +# CMAKE_CXX_FLAGS +# + +# Basic sanity check: +if(${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_BINARY_DIR}) + message(FATAL_ERROR "Avoid building inside the source tree. Create a `build` directory?") +endif() + +set(CMAKE_DEBUG_POSTFIX d) # appends d to libraries. + +if(MSVC) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_BINARY_DIR}/Bin) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_BINARY_DIR}/Bin) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO ${CMAKE_BINARY_DIR}/Bin) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_MINSIZEREL ${CMAKE_BINARY_DIR}/Bin) + + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELEASE ${CMAKE_BINARY_DIR}/Bin) # for .dll files + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_DEBUG ${CMAKE_BINARY_DIR}/Bin) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELWITHDEBINFO ${CMAKE_BINARY_DIR}/Bin) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_MINSIZEREL ${CMAKE_BINARY_DIR}/Bin) + + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELEASE ${CMAKE_BINARY_DIR}/Bin) + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_DEBUG ${CMAKE_BINARY_DIR}/Bin) + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELWITHDEBINFO ${CMAKE_BINARY_DIR}/Bin) + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_MINSIZEREL ${CMAKE_BINARY_DIR}/Bin) +else() + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_BINARY_DIR}/Bin) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_BINARY_DIR}/Bin) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO ${CMAKE_BINARY_DIR}/Bin) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_MINSIZEREL ${CMAKE_BINARY_DIR}/Bin) + + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELEASE ${CMAKE_BINARY_DIR}/Bin) # for .so files + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_DEBUG ${CMAKE_BINARY_DIR}/Bin) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELWITHDEBINFO ${CMAKE_BINARY_DIR}/Bin) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_MINSIZEREL ${CMAKE_BINARY_DIR}/Bin) + + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELEASE ${CMAKE_BINARY_DIR}/Lib) # for .a files + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_DEBUG ${CMAKE_BINARY_DIR}/Lib) + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELWITHDEBINFO ${CMAKE_BINARY_DIR}/Lib) + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_MINSIZEREL ${CMAKE_BINARY_DIR}/Lib) +endif() + + +if( APPLE OR UNIX OR MINGW ) + + if(CMAKE_SIZEOF_VOID_P EQUAL 8) + # go ahead and search for 64 bit libs + set_property(GLOBAL PROPERTY FIND_LIBRARY_USE_LIB64_PATHS ON) + endif() + + set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Werror" ) + set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fvisibility=hidden" ) + set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fvisibility-inlines-hidden" ) + set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wfloat-equal") + set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wshadow") + + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-return-type-c-linkage") + endif() + + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + # Colorize output + set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fdiagnostics-color=always" ) + + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wctor-dtor-privacy -Wnon-virtual-dtor" ) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wsuggest-override" ) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wredundant-decls") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wlogical-op") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wold-style-cast") + + if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "8.3.0") + # do not enable in earlier versions due to gcc defect 83591 + # https://gcc.gnu.org/bugzilla/show_bug.cgi?id=83591 + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wduplicated-branches" ) + endif() + endif() + +elseif(MSVC) + # Increase warning level and disable warnings on an individual basis. + string(REPLACE "/W3" "" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}") + string(REPLACE "/W3" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") + set( CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /W4 /WX" ) + set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4 /WX" ) + + #set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4275" ) # example, also provide REASON for disabling. + + # Enable multi processor compilations as default: + set( CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /MP ") + set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP ") + + # Set compatibility to Windows 10. + add_definitions(-DWINVER=0x0A00 -D_WIN32_WINNT=0x0A00) + + add_definitions(-DCRT_NO_DEPRECATE -D_CRT_SECURE_NO_WARNINGS) # Prevent various warnings. + add_definitions(-DNOMINMAX) # `windows.h` functions interfere with std min & max in `algorithm`. +endif() + + +SET_PROPERTY(DIRECTORY APPEND PROPERTY COMPILE_DEFINITIONS $<$:_DEBUG> ) +SET_PROPERTY(DIRECTORY APPEND PROPERTY COMPILE_DEFINITIONS $<$:NDEBUG> ) +SET_PROPERTY(DIRECTORY APPEND PROPERTY COMPILE_DEFINITIONS $<$:NDEBUG> ) +SET_PROPERTY(DIRECTORY APPEND PROPERTY COMPILE_DEFINITIONS $<$:NDEBUG> ) + +if(CMAKE_BUILD_TYPE STREQUAL Debug) + add_definitions(-D_DEBUG) +elseif(CMAKE_BUILD_TYPE STREQUAL Release) + add_definitions(-DNDEBUG) +elseif(CMAKE_BUILD_TYPE STREQUAL RelWithDebInfo) + add_definitions(-DNDEBUG) +elseif(CMAKE_BUILD_TYPE STREQUAL MinSizeRel) + # +else() + # Seems the build type isn't set. Tell the users. Unless it's visual studio. + if(NOT MSVC) + message(FATAL_ERROR "\nSet CMAKE_BUILD_TYPE to one of: Release, Debug, RelWithDebInfo, or MinSizeRel\n e.g. `cmake .. -DCMAKE_BUILD_TYPE=Debug`\n" ) + endif() +endif() diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt new file mode 100644 index 0000000..9992b74 --- /dev/null +++ b/common/CMakeLists.txt @@ -0,0 +1,27 @@ +set( target_name antler_common ) + +# I know. But it's appropriate to glob for now. +file(GLOB_RECURSE src_cpp ./src/*.cpp) +file(GLOB_RECURSE src_h ./src/*.h ./src/*.hpp) +file(GLOB_RECURSE inc_h ./antler/common/*.h ./antler/common/*.hpp) + +add_library(${target_name} ${src_cpp} ${src_h} ${inc_h}) +set_property(TARGET ${target_name} PROPERTY CXX_STANDARD 20) +target_compile_definitions(${target_name} PRIVATE + BUILDING_${target_name} +) +target_link_libraries(${target_name} PUBLIC +) +target_include_directories(${target_name} PRIVATE + src +) +target_include_directories(${target_name} PUBLIC + . +) + + +# Install commands +install( + FILES ${inc_h} + DESTINATION ${target_name} COMPONENT Development +) diff --git a/common/antler/common/so_support.h b/common/antler/common/so_support.h new file mode 100644 index 0000000..812e41f --- /dev/null +++ b/common/antler/common/so_support.h @@ -0,0 +1,32 @@ +#ifndef antler_so_support_h +#define antelr_so_support_h + + +#ifdef __cplusplus +extern "C" { +#endif + +// Support for shared libs. + + +#if defined(_WIN32) +# define SO_IMPORT __declspec(dllimport) +# define SO_EXPORT __declspec(dllexport) +# define SO_LOCAL +# define SO_NEITHER +#elif defined (__GNUC__) +# define SO_IMPORT __attribute__ ((visibility("default"))) +# define SO_EXPORT __attribute__ ((visibility("default"))) +# define SO_LOCAL __attribute__ ((visibility("hidden"))) +# define SO_NEITHER __attribute__ ((visibility("default"))) +#else +# error "Your comiler needs Shared Object defines!" +#endif + + + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif diff --git a/common/antler/string/detail/from.ipp b/common/antler/string/detail/from.ipp new file mode 100644 index 0000000..dd81fdb --- /dev/null +++ b/common/antler/string/detail/from.ipp @@ -0,0 +1,36 @@ +#ifndef antler_string_from_h +# error "bad inclusion" +#endif + + + +namespace antler { +namespace string { + + +template +inline bool from(std::string_view s, T& n) { + n = 0; + for(auto c : s) { + n *= 10; + if(c < '0' || c > '9') + return false; + n += c-'0'; + } + return true; +} + + +// Specialization for ensure a char comes in as a uint8_t. +template<> +inline bool from(std::string_view s, uint8_t& rv) { + unsigned u; + if (!from(s,u)) + return false; + rv = uint8_t(u); + return true; +} + + +} // namespace string +} // namespace antler diff --git a/common/antler/string/detail/split.ipp b/common/antler/string/detail/split.ipp new file mode 100644 index 0000000..5af484d --- /dev/null +++ b/common/antler/string/detail/split.ipp @@ -0,0 +1,30 @@ +#ifndef antler_string_split_h +# error "bad inclusion" +#endif + + + +namespace antler { +namespace string { + + +template +inline std::vector split(std::string_view s, std::string_view split_chars) { + std::vector rv; + while(!s.empty()) { + auto r = s.find_first_of(split_chars); + if( r == std::string_view::npos) { + rv.push_back( STRING_TYPE(s) ); + return rv; + } + if(r != 0) + rv.push_back( STRING_TYPE(s.substr(0,r)) ); + s = s.substr(r+1); + } + return rv; +} + + + +} // namespace string +} // namespace antler diff --git a/common/antler/string/detail/trim.ipp b/common/antler/string/detail/trim.ipp new file mode 100644 index 0000000..f73b6a1 --- /dev/null +++ b/common/antler/string/detail/trim.ipp @@ -0,0 +1,36 @@ +#ifndef antler_string_trim_h +# error "bad inclusion" +#endif + + + +namespace antler { +namespace string { + + +inline std::string_view trim(std::string_view s) { + if(s.empty()) + return s; + + // trim front, first iteration is guaranteed valid. + size_t i=0; + while(std::isspace(s[i])) { + ++i; + // Exit if the string is ALL whitespace. + if(i == s.size()) + return std::string_view{}; + } + s = s.substr(i); + + // trim rear, first iteration is guaranteed valid. + i=s.size()-1; + while(std::isspace(s[i])) + --i; + + return s.substr(0,i+1); +} + + + +} // namespace string +} // namespace antler diff --git a/common/antler/string/from.h b/common/antler/string/from.h new file mode 100644 index 0000000..c5df001 --- /dev/null +++ b/common/antler/string/from.h @@ -0,0 +1,37 @@ +#ifndef antler_string_from_h +#define antler_string_from_h + +/* + Sourced from libsb +*/ + + +#include + +namespace antler { +namespace string { + +/// Convert text to a numeric value. Works for int and unsigned of all widths. +/// @warn Does NOT work for negative numbers. +/// @warn The string must contain ONLY valid digits from 0 to 9. +/// @warn Does not work for hex, octal, or binary numbers. +/// @example +/// uint64_t num; +/// if(!antler::string::from("25",num) { +/// /* do error */ +/// } +/// else { +/// std::cout << " Received value: " << num << "\n"; +/// } +/// @param s The text source to convert. +/// @param rv This is a return value, it's updated if s was convertable to a T. +/// @return Returns true if rv was updated, false otherwise. +template +bool from(std::string_view s, T& rv); + +} // namespace string +} // namespace antler + +#include + +#endif diff --git a/common/antler/string/split.h b/common/antler/string/split.h new file mode 100644 index 0000000..a8be5cc --- /dev/null +++ b/common/antler/string/split.h @@ -0,0 +1,20 @@ +#ifndef antler_string_split_h +#define antler_string_split_h + +#include +#include + +namespace antler { +namespace string { + +/// @return a vector of parsed values. Note that empty values are ignored. +template +std::vector split(std::string_view s, std::string_view split_chars); + + +} // namespace string +} // namespace antler + +#include + +#endif diff --git a/common/antler/string/trim.h b/common/antler/string/trim.h new file mode 100644 index 0000000..32ffb34 --- /dev/null +++ b/common/antler/string/trim.h @@ -0,0 +1,20 @@ +#ifndef antler_string_trim_h +#define antler_string_trim_h + +#include + +namespace antler { +namespace string { + +/// Trim whitespace from a string_view. +/// @note May return an empty string. +/// @return A string view with whitespace trimmed from front and rear. +std::string_view trim(std::string_view s); + + +} // namespace string +} // namespace antler + +#include + +#endif diff --git a/common/antler/system/detail/exec.ipp b/common/antler/system/detail/exec.ipp new file mode 100644 index 0000000..d8936a3 --- /dev/null +++ b/common/antler/system/detail/exec.ipp @@ -0,0 +1,40 @@ +#ifndef antler_system_exec_h +# error "bad inclusion" +#endif + +#include +#include +#include + +namespace antler { +namespace system { + +inline result exec(std::string_view cmd_in) noexcept { + + // We get stdout + stderr. + std::string cmd(cmd_in); + cmd += " 2>&1"; + + result rv; + + // Open the pipe... + FILE* pipe = popen(cmd.c_str(), "r"); + if(!pipe) { + // ...let the user know on failure. + rv.return_code = -1; + rv.output = "antler::system::exec() error: failed to open pipe."; + return rv; + } + + std::array buffer; + while( fgets(buffer.data(), buffer.size(), pipe) != nullptr) + rv.output += buffer.data(); + + rv.return_code = pclose(pipe); + + return rv; +} + + +} // namespace system +} // namespace antler diff --git a/common/antler/system/exec.h b/common/antler/system/exec.h new file mode 100644 index 0000000..297f290 --- /dev/null +++ b/common/antler/system/exec.h @@ -0,0 +1,35 @@ +#ifndef antler_system_exec_h +#define antler_system_exec_h + +#include +#include + + +namespace antler { +namespace system { + +struct result { + + int return_code=0; + std::string output; + + + // bool operator returns true when error state is set, false otherwise + // implementation for bool operator + typedef void (*unspecified_bool_type)(); + static void unspecified_bool_true() {;} + operator unspecified_bool_type() const { return (return_code != 0 ? 0 : unspecified_bool_true); } + + /// @return true when there is NO error; false for error (oppositie of the bool operator + bool operator!() const { return return_code != 0; } +}; + + +result exec(std::string_view cmd) noexcept; + +} // namespace system +} // namespace antler + +#include + +#endif diff --git a/common/sb/filesystem/detail/executable_path.ipp b/common/sb/filesystem/detail/executable_path.ipp new file mode 100644 index 0000000..8412a3c --- /dev/null +++ b/common/sb/filesystem/detail/executable_path.ipp @@ -0,0 +1,105 @@ +#ifndef sb_filesystem_executable_path_h +# error "Direct inclusion error." +#endif + +/* + Copyright (c) 2022, Scott Bailey + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL SCOTT BAILEY BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + + + +#if defined(__linux__) +# include // PATH_MAX +# include // memset() +# include // readlink() +#elif defined(__APPLE__) +# include // _NSGetExecutablePath() +#elif defined(_WIN32) +# include +# include // GetModuleFileNameW() +# include // GetLastError() +# include // _MAX_PATH +#else +# error "Need to implement for your OS" +#endif + + + + +namespace sb { +namespace filesystem { + +inline std::filesystem::path executable_path() { + +#if defined(__linux__) + char dest[PATH_MAX]; + memset(dest,0,sizeof(dest)); // readlink does not null terminate! + + if (readlink("/proc/self/exe", dest, PATH_MAX) == -1) { + perror("readlink"); + } + +#elif defined(__APPLE__) + char dest[PATH_MAX]; + memset(dest,0,sizeof(dest)); // readlink does not null terminate! + + uint32_t size = sizeof(dest); + if (_NSGetExecutablePath(dest, &size) != 0) { + perror("_NSGetExecutablePath"); + } + +#elif defined(_WIN32) + wchar_t dest[_MAX_PATH]; + if(!GetModuleFileNameW( NULL, dest, _MAX_PATH )) { + std::cerr << "GetModuleFileNameW() error: " << GetLastError() << std::endl; + } + +#else +# error "Need to implement for your OS" +#endif + + std::error_code sec; + + // Conversion to an absolute path is probably unnecessary; however this should be called rarely so a small performance hit is + // acceptable. + auto rv = std::filesystem::absolute(dest,sec); + if(sec) { + std::cerr << "get_executable_path() - absolute() error: " << sec << std::endl; + } + + // Following the symlink is a requirement! + if(std::filesystem::is_symlink(rv,sec)) { + rv = std::filesystem::read_symlink(rv,sec); + } + if(sec) { + std::cerr << "get_executable_path() - read_symlink() error: " << sec << std::endl; + } + + return rv; +} + +} // namespace filesystem +} // namespace sb diff --git a/common/sb/filesystem/executable_path.h b/common/sb/filesystem/executable_path.h new file mode 100644 index 0000000..3135f79 --- /dev/null +++ b/common/sb/filesystem/executable_path.h @@ -0,0 +1,47 @@ +#ifndef sb_filesystem_executable_path_h +#define sb_filesystem_executable_path_h + +/* + Copyright (c) 2022, Scott Bailey + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL SCOTT BAILEY BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + + +#include + +namespace sb { +namespace filesystem { + +/// Find the path to the current executable. +/// @return Absolute path to current executbale. +std::filesystem::path executable_path(); + +} // namespace filesystem +} // namespace sb + + +#include + +#endif diff --git a/common/sb/ignore.h b/common/sb/ignore.h new file mode 100644 index 0000000..8629709 --- /dev/null +++ b/common/sb/ignore.h @@ -0,0 +1,13 @@ +#ifndef sb_ignore_h +#define sb_ignore_h + +namespace sb +{ + +template +inline void ignore(const Ts&...) {} + +} + + +#endif diff --git a/common/src/lib.cpp b/common/src/lib.cpp new file mode 100644 index 0000000..78f1ca1 --- /dev/null +++ b/common/src/lib.cpp @@ -0,0 +1,3 @@ + +void something() { +} diff --git a/common/src/so_support.h b/common/src/so_support.h new file mode 100644 index 0000000..e4ab8d3 --- /dev/null +++ b/common/src/so_support.h @@ -0,0 +1 @@ +#include diff --git a/depends/CMakeLists.txt b/depends/CMakeLists.txt new file mode 100644 index 0000000..57faa39 --- /dev/null +++ b/depends/CMakeLists.txt @@ -0,0 +1,13 @@ + +include(FetchContent) +include(ExternalProject) + +if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.24") + # Set timestamp policy to EXTRACTION time instead of archive time. + cmake_policy(SET CMP0135 NEW) +endif() + +include(./rapidyaml.cmake REQUIRED) +#include(./yaml-cpp.cmake REQUIRED) +#include(./yaml.cmake REQUIRED) +#include(./toml.cmake REQUIRED) diff --git a/depends/patches/rapidyaml.patch b/depends/patches/rapidyaml.patch new file mode 100644 index 0000000..0bc9be0 --- /dev/null +++ b/depends/patches/rapidyaml.patch @@ -0,0 +1,69 @@ +diff -r --unified ryml-original/CMakeLists.txt ryml-src/CMakeLists.txt +index d18407c..5c2f75c 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -6,6 +6,9 @@ project(ryml + LANGUAGES CXX) + include(./compat.cmake) + ++set_property(GLOBAL PROPERTY CXX_STANDARD "17") ++set(CXX_STANDARD "17") ++ + c4_project(VERSION 0.4.1 STANDALONE + AUTHOR "Joao Paulo Magalhaes ") + +@@ -59,6 +61,7 @@ c4_add_library(ryml + LIBS c4core + INCORPORATE c4core + ) ++set_property(TARGET ryml PROPERTY CXX_STANDARD 17) + + if(RYML_WITH_TAB_TOKENS) + target_compile_definitions(ryml PUBLIC RYML_WITH_TAB_TOKENS) +diff -r --unified ryml-original/ext/c4core/src/c4/substr.hpp ryml-src/ext/c4core/src/c4/substr.hpp +--- ryml-original/ext/c4core/src/c4/substr.hpp 2022-12-07 09:15:04.479783880 -0600 ++++ ryml-src/ext/c4core/src/c4/substr.hpp 2022-12-07 09:12:02.768022161 -0600 +@@ -3,6 +3,7 @@ + + /** @file substr.hpp read+write string views */ + ++#include + #include + #include + #include +@@ -102,6 +103,9 @@ + /// convert automatically to substring of const C + operator ro_substr () const { ro_substr s(str, len); return s; } + ++ ++ operator const std::string_view () const { return std::string_view(str,len); } ++ + /** @} */ + + public: +diff --git a/src/c4/std/string.hpp b/src/c4/std/string.hpp +index 7c08df0..3ea9cd4 100644 +--- a/ext/c4core/src/c4/std/string.hpp ++++ b/ext/c4core/src/c4/std/string.hpp +@@ -8,6 +8,7 @@ + #endif + + #include ++#include + + namespace c4 { + +@@ -27,6 +28,13 @@ inline c4::csubstr to_csubstr(std::string const& s) + return c4::csubstr(data, s.size()); + } + ++/** get a readonly view to an existing std::string_view */ ++inline c4::csubstr to_csubstr(std::string_view const s) ++{ ++ const char* data = ! s.empty() ? &s[0] : nullptr; ++ return c4::csubstr(data, s.size()); ++} ++ + //----------------------------------------------------------------------------- + + C4_ALWAYS_INLINE bool operator== (c4::csubstr ss, std::string const& s) { return ss.compare(to_csubstr(s)) == 0; } diff --git a/depends/patches/toml.patch b/depends/patches/toml.patch new file mode 100644 index 0000000..d8ad2e8 --- /dev/null +++ b/depends/patches/toml.patch @@ -0,0 +1,32 @@ +diff -uN tomlc99-894902820a3ea2f1ec470cd7fe338bde54045cf5/CMakeLists.txt tomlc99-894902820a3ea2f1ec470cd7fe338bde54045cf52/CMakeLists.txt +--- tomlc99-894902820a3ea2f1ec470cd7fe338bde54045cf5/CMakeLists.txt 1969-12-31 18:00:00.000000000 -0600 ++++ tomlc99-894902820a3ea2f1ec470cd7fe338bde54045cf52/CMakeLists.txt 2022-12-01 15:00:18.867019565 -0600 +@@ -0,0 +1,28 @@ ++# ++# Add the library ++# ++add_library(toml STATIC toml.h toml.c) ++target_include_directories(toml PUBLIC ++ $ ++ $ ++ $ ++ ) ++ ++ ++# ++# Install rules ++# ++install( ++ FILES ++ toml.h ++ DESTINATION include COMPONENT Development ++ ) ++ ++install( ++ TARGETS toml ++ EXPORT tomlTargets ++ RUNTIME DESTINATION "${INSTALL_BIN_DIR}" COMPONENT Runtime ++ LIBRARY DESTINATION "${INSTALL_LIB_DIR}" COMPONENT Development ++ ARCHIVE DESTINATION "${INSTALL_LIB_DIR}" COMPONENT Development ++ ) ++ diff --git a/depends/patches/yaml.patch b/depends/patches/yaml.patch new file mode 100644 index 0000000..aa3b2b5 --- /dev/null +++ b/depends/patches/yaml.patch @@ -0,0 +1,13 @@ +Common subdirectories: yaml-src/.github and yaml-src-mod/.github +diff -uN yaml-src/CMakeLists.txt yaml-src-mod/CMakeLists.txt +--- yaml-src/CMakeLists.txt 2022-12-01 16:14:14.224201830 -0600 ++++ yaml-src-mod/CMakeLists.txt 2022-12-01 16:16:07.654574375 -0600 +@@ -100,7 +100,7 @@ + # + include(CTest) # This module defines BUILD_TESTING option + if(BUILD_TESTING) +- add_subdirectory(tests) ++# add_subdirectory(tests) + endif() + + # diff --git a/depends/rapidyaml.cmake b/depends/rapidyaml.cmake new file mode 100644 index 0000000..643238e --- /dev/null +++ b/depends/rapidyaml.cmake @@ -0,0 +1,57 @@ +# rapidyaml (ryml) dependency + + +if(true) + + FetchContent_Declare( + ryml + SYSTEM + GIT_REPOSITORY https://github.com/biojppm/rapidyaml.git + # Always use a full commit hash to ensure a tag change in the source + # repo doesn't change the behavior of our code. + GIT_TAG 213b201d264139cd1b887790197e08850af628e3 # v0.4.1 + + # Patch that allows for: + # `operator std::string_view()` + # C++17 standard + PATCH_COMMAND patch -p1 < ${CMAKE_CURRENT_SOURCE_DIR}/patches/rapidyaml.patch + ) + + FetchContent_MakeAvailable(ryml) + +endif() + + + +if(false) + + # For whatever reason, this path doesn't work. + # + # The reason to prefer this route in package building is that files + # are easier to mirror than github repos. + + FetchContent_Declare( + ryml + SYSTEM + URL https://github.com/biojppm/rapidyaml/archive/refs/tags/v0.4.1.tar.gz + URL_HASH SHA256=b4ef0300b5899ede26ed529d8a8daa3347810693957707dbe522f69b17250ae2 + DOWNLOAD_NAME rapidyaml-v0.4.1.tar.gz + ) + + FetchContent_Populate(ryml) + + FetchContent_Declare( + c4core + SYSTEM + SOURCE_DIR ${ryml_SOURCE_DIR}/ext/c4core/ + URL https://github.com/biojppm/c4core/archive/refs/tags/v0.1.9.tar.gz + URL_HASH SHA256=12aba7d04e77ce5e7c5edc3165e700f7314b0f67e3706ad285bb7cb2020c52af + DOWNLOAD_NAME c4core-v0.1.9.tar.gz + ) + + FetchContent_Populate(c4core) + + + FetchContent_MakeAvailable(ryml) + +endif() diff --git a/depends/toml.cmake b/depends/toml.cmake new file mode 100644 index 0000000..672705c --- /dev/null +++ b/depends/toml.cmake @@ -0,0 +1,11 @@ + + +set(TOML_GIT_HASH 894902820a3ea2f1ec470cd7fe338bde54045cf5) +FetchContent_Declare( + toml + URL https://github.com/cktan/tomlc99/archive/${TOML_GIT_HASH}.tar.gz + URL_HASH SHA256=3ae1970c4f2d03ac16f8616963dfa6d1fac38883408f76fefbecfc1d8f61e570 + PATCH_COMMAND patch -p1 < ${CMAKE_CURRENT_SOURCE_DIR}/patches/toml.patch +) + +FetchContent_MakeAvailable(toml) diff --git a/depends/yaml-cpp.cmake b/depends/yaml-cpp.cmake new file mode 100644 index 0000000..336ad09 --- /dev/null +++ b/depends/yaml-cpp.cmake @@ -0,0 +1,11 @@ + + +FetchContent_Declare( + yaml-cpp + SYSTEM + URL https://github.com/jbeder/yaml-cpp/archive/refs/tags/yaml-cpp-0.7.0.tar.gz + URL_HASH SHA256=43e6a9fcb146ad871515f0d0873947e5d497a1c9c60c58cb102a97b47208b7c3 + #PATCH_COMMAND patch -p1 < ${CMAKE_CURRENT_SOURCE_DIR}/patches/yaml.patch # Disable building test files. +) + +FetchContent_MakeAvailable(yaml-cpp) diff --git a/depends/yaml.cmake b/depends/yaml.cmake new file mode 100644 index 0000000..db46ad4 --- /dev/null +++ b/depends/yaml.cmake @@ -0,0 +1,10 @@ + + +FetchContent_Declare( + yaml + URL https://github.com/yaml/libyaml/archive/refs/tags/0.2.5.tar.gz + URL_HASH SHA256=fa240dbf262be053f3898006d502d514936c818e422afdcf33921c63bed9bf2e + PATCH_COMMAND patch -p1 < ${CMAKE_CURRENT_SOURCE_DIR}/patches/yaml.patch # Disable building test files. +) + +FetchContent_MakeAvailable(yaml) diff --git a/docs/project_manager.md b/docs/project_manager.md index dc982ef..893b633 100644 --- a/docs/project_manager.md +++ b/docs/project_manager.md @@ -1,234 +1,239 @@ -## Project Manager -The project manager system will define a convention and specification for Antelope Smart Contract Packages. - -### New tool antler-pack / cdt-pack -This tool will have functionality to create a new project and generate the correct directory tree and stub files. - -This tool will be used to pull other packages dependencies listed in the manifest. - -This tool will allow the contract writer to add new dependencies and sub sections of their app. - -Github will act as the primary 'repo' for packages to exist. When listing dependencies a few options are available: -1) Full URL of git project -2) Shorthand i.e. Org/Repo (example: antelopeio/safe_math) -3) An optional version can be added with the additional qualifier of greater than '>' - -#### Manifest -Example project.yml: -```yaml ---- - project: "antelope_system_contract" - version: "1.0.0" - libraries: - - name: "core" - lang: "C++" - options: "-O0" - - name: "something" - lang: "C" - options: "-fno-something" - apps: - - name: "sys_contract" - lang: "C++" - depends: - - name: "core" - - name: "something" - - name: "fast_math/math" - from: "https://github.com/larryk85/fast_math" - version: 3.0.1 - - name: "SomeLibrary/lib" - from: "AntelopeIO/SomeLibrary" - version: 1.0.2 - - name: "other" - lang: "https://github.com/JavaJava/Java" - depends: - - name: "JavaLib/JavaLib" - from: "https://github.com/JavaJava/JavaLib" - version: 10.3.4 - tests: - - name: "main_tests" - depends: - - name: "core" - - name: "something" - command: "./main_tests" - - name: "functional_tests" - depends: - - name: "sys_contract" - - name: "Catch2/Catch2" - from: "catchorg/Catch2" - command: "./functional_tests -verbose=0" -``` - -The directory structure for the project manager system will be as such: -``` -──project_root - ├───apps - ├───include - ├───ricardian - ├───libs - ├───tests - └───project.yml -``` -### Convention and Specification -A strict convention will be used to define packages/projects in terms of directory layout and manifest files. - -### antler-pack --init -`antler-pack --init` should produce an initial project for the developer with the above given directory structure and project.yml. - -Prompts should be given for: -- Project name -- Project version -- App/s this project will create - - name of the app - - programming language of the app - - dependencies of the app -- Lib/s this project will create - - name of the library - - programming language of the library - - dependencies of the library - -### antler-pack --add-app -`antler-pack --add-app` will allow the user to add a new app to their project given a current project exists. - -Prompts should be given for: -- App name -- App programming language -- App dependencies - - dependency name - - dependency location (local or github) - - dependency version (if location is local then ignore) - -With this information a new entry in the `project.yml` will be created. - -### antler-pack --add-lib -`antler-pack --add-lib` will allow the user to add a new library to their project given a current project exists. - -Prompts should be given for: -- library name -- library programming language -- library dependencies - - dependency name - - dependency location (local or github) - - dependency version (if location is local then ignore) - -With this information a new entry in the `project.yml` will be created. - -### antler-pack --add-test -`antler-pack --add-test` will allow the user to add a new test to their project given a current project exists. - -Prompts should be given for: -- test name -- test dependencies - - dependency name - - dependency location (local or github) - - dependency version (if location is local then ignore) - -With this information a new entry in the `project.yml` will be created. - -### antler-pack --add-dependency -`antler-pack --add-dependency` will allow the user to add a dependency to their project. - -Prompts should be given for: -- Dependency name -- Dependency location (local or github) -- Dependency version (if location is local then ignore) -- App/s names that will use this dependency -- Lib/s names that will use this dependency -- Test/s names that will use this dependency - -With this information the associated app/s and lib/s in the `project.yml` will be updated. - -### antler-pack --remove-dependency -`antler-pack --remove-dependency` will allow the user to remove a dependency from their project. - -Prompts should be given for: -- Dependency name -- App/s to remove the dependency from -- Lib/s to remove the dependency from -- Test/s to remove the dependency from - -With this information the associated app/s and lib/s in the `project.yml` will be updated. - -A default value for 'project all' should be given for the prompts to allow the dependency to be removed completely. - -### antler-pack --update-dependency -`antler-pack --update-dependency` will allow the user to update a given dependency in their project. - -Prompts should be given for: -- Dependency name -- New location? -- New version? - -With this information the tool will update the associated `project.yml`. - -### antler-pack --populate -`antler-pack --populate` should download any dependencies need for the project. - -### C++ project.yml parsing library -The following structures and functions should exist. - -```C++ -namespace antler { - -struct version { - uint16_t major; - uint16_t minor; - uint16_t patch; -}; - -class dependency { - public: - // use default constructors, copy and move constructors and assignments - void update_dependency(version v, std::string&& location) noexcept; - version get_version() const noexcept; - std::string_view get_location() const noexcept; - private: - std::string name; - version ver; - std::string location; -}; - -class object { - public: - // use default constructors, copy and move constructors and assignments - - void upsert_dependency(dependency&& dep) noexcept; - // returns true if successful, false if the fails (i.e. dependency does not exist) - bool remove_dependency(std::string_view name) noexcept; - - void update_options(std::string&& options) noexcept; - void update_language(std::string&& language) noexcept; - std::string_view get_name() const noexcept; - std::string_view get_options() const noexcept; - std::string_view get_language() const noexcept; - - const std::vector* get_dependencies() const noexcept; - - private: - std::string name; - std::string options; - std::string language; - std::unordered_set dependencies; -}; - -// main structure for the project.yml file -class project { - public: - // parse a project from a project.yml - inline project(const char* filename); - void upsert_app(object&& app) noexcept; - void upsert_lib(object&& lib) noexcept; - void upsert_test(object&& test) noexcept; - const std::unordered_set get_apps() const noexcept; - const std::unordered_set get_libs() const noexcept; - const std::unordered_set get_tests() const noexcept; - private: - std::string file_location; - std::string project_name; - version ver; - std::unordered_set apps; - std::unordered_set libs; - std::unordered_set tests; -}; - -} // ns antler -``` \ No newline at end of file +## Project Manager +The project manager system will define a convention and specification for Antelope Smart Contract Packages. + +### New tool antler-pack / cdt-pack +This tool will have functionality to create a new project and generate the correct directory tree and stub files. + +This tool will be used to pull other packages dependencies listed in the manifest. + +This tool will allow the contract writer to add new dependencies and sub sections of their app. + +Github will act as the primary 'repo' for packages to exist. When listing dependencies a few options are available: +1) Full URL of git project +2) Shorthand i.e. Org/Repo (example: antelopeio/safe_math) +3) An optional version can be added with the additional qualifier of greater than '>' + +#### Manifest +Example project.yml: +```yaml +--- + project: "antelope_system_contract" + version: "1.0.0" + libraries: + - name: "core" + lang: "C++" + options: "-O0" + - name: "something" + lang: "C" + options: "-fno-something" + apps: + - name: "sys_contract" + lang: "C++" + depends: + - name: "core" + - name: "something" + - name: "fast_math/math" + from: "https://github.com/larryk85/fast_math" + version: 3.0.1 + - name: "SomeLibrary/lib" + from: "AntelopeIO/SomeLibrary" + version: 1.0.2 + - name: "other" + lang: "https://github.com/JavaJava/Java" + depends: + - name: "JavaLib/JavaLib" + from: "https://github.com/JavaJava/JavaLib" + version: 10.3.4 + tests: + - name: "main_tests" + depends: + - name: "core" + - name: "something" + command: "./main_tests" + - name: "functional_tests" + depends: + - name: "sys_contract" + - name: "Catch2/Catch2" + from: "catchorg/Catch2" + command: "./functional_tests -verbose=0" +``` + +The directory structure for the project manager system will be as such: +``` +──project_root + ├───apps + ├───include + ├───ricardian + ├───libs + ├───tests + └───project.yml +``` +### Convention and Specification +A strict convention will be used to define packages/projects in terms of directory layout and manifest files. + +### antler-pack --init +`antler-pack --init` should produce an initial project for the developer with the above given directory structure and project.yml. + +Prompts should be given for: +- Project name +- Project version +- App/s this project will create + - name of the app + - programming language of the app + - dependencies of the app +- Lib/s this project will create + - name of the library + - programming language of the library + - dependencies of the library + +### antler-pack --add-app +`antler-pack --add-app` will allow the user to add a new app to their project given a current project exists. + +Prompts should be given for: +- App name +- App programming language +- App dependencies + - dependency name + - dependency location (local or github) + - dependency version (if location is local then ignore) + +With this information a new entry in the `project.yml` will be created. + +### antler-pack --add-lib +`antler-pack --add-lib` will allow the user to add a new library to their project given a current project exists. + +Prompts should be given for: +- library name +- library programming language +- library dependencies + - dependency name + - dependency location (local or github) + - dependency version (if location is local then ignore) + +With this information a new entry in the `project.yml` will be created. + +### antler-pack --add-test +`antler-pack --add-test` will allow the user to add a new test to their project given a current project exists. + +Prompts should be given for: +- test name +- test dependencies + - dependency name + - dependency location (local or github) + - dependency version (if location is local then ignore) + +With this information a new entry in the `project.yml` will be created. + +### antler-pack --add-dependency +`antler-pack --add-dependency` will allow the user to add a dependency to their project. + +Prompts should be given for: +- Dependency name +- Dependency location (local or github) +- Dependency version (if location is local then ignore) +- App/s names that will use this dependency +- Lib/s names that will use this dependency +- Test/s names that will use this dependency + +With this information the associated app/s and lib/s in the `project.yml` will be updated. + +### antler-pack --remove-dependency +`antler-pack --remove-dependency` will allow the user to remove a dependency from their project. + +Prompts should be given for: +- Dependency name +- App/s to remove the dependency from +- Lib/s to remove the dependency from +- Test/s to remove the dependency from + +With this information the associated app/s and lib/s in the `project.yml` will be updated. + +A default value for 'project all' should be given for the prompts to allow the dependency to be removed completely. + +### antler-pack --update-dependency +`antler-pack --update-dependency` will allow the user to update a given dependency in their project. + +Prompts should be given for: +- Dependency name +- New location? +- New version? + +With this information the tool will update the associated `project.yml`. + +### antler-pack --populate +`antler-pack --populate` should download any dependencies need for the project. + +### C++ project.yml parsing library +The following structures and functions should exist. + +```C++ +namespace antler { + +struct version { + uint16_t major; + uint16_t minor; + uint16_t patch; +}; + +class dependency { + public: + // use default constructors, copy and move constructors and assignments + void update_dependency(version v, std::string&& location) noexcept; + version get_version() const noexcept; + std::string_view get_location() const noexcept; + private: + std::string name; + version ver; + std::string location; +}; + +class object { + public: + // use default constructors, copy and move constructors and assignments + + void upsert_dependency(dependency&& dep) noexcept; + // returns true if successful, false if the fails (i.e. dependency does not exist) + bool remove_dependency(std::string_view name) noexcept; + + void update_options(std::string&& options) noexcept; + void update_language(std::string&& language) noexcept; + std::string_view get_name() const noexcept; + std::string_view get_options() const noexcept; + std::string_view get_language() const noexcept; + + const std::vector* get_dependencies() const noexcept; + + private: + std::string name; + std::string options; + std::string language; + std::unordered_set dependencies; +}; + +// main structure for the project.yml file +class project { + public: + // parse a project from a project.yml + inline project(const char* filename); + void upsert_app(object&& app) noexcept; + void upsert_lib(object&& lib) noexcept; + void upsert_test(object&& test) noexcept; + const std::unordered_set get_apps() const noexcept; + const std::unordered_set get_libs() const noexcept; + const std::unordered_set get_tests() const noexcept; + private: + std::string file_location; + std::string project_name; + version ver; + std::unordered_set apps; + std::unordered_set libs; + std::unordered_set tests; +}; + +} // ns antler +``` + +# Example projects + +https://github.com/larryk85/sample_contract +https://github.com/larryk85/test_lib diff --git a/docs/sample1.yaml b/docs/sample1.yaml new file mode 100644 index 0000000..5588f80 --- /dev/null +++ b/docs/sample1.yaml @@ -0,0 +1,44 @@ +project: "antelope_system_contract" +version: "1.0.0" + +libraries: + - name: "core" + lang: "C++" + options: "-O0" + - name: "something" + lang: "C" + options: "-fno-something" +apps: + - name: "sys_contract" + lang: "C++" + depends: + - name: "core" + - name: "something" + - name: "fast_math/math" + from: "https://github.com/larryk85/fast_math" + version: 3.0.1 + patch: + - "from/here.patch" + - "from/there.patch" + - name: "SomeLibrary/lib" + from: "AntelopeIO/SomeLibrary" + version: 1.0.2 + - name: "other" + #lang: "https://github.com/JavaJava/Java" + lang: "Java" + depends: + - name: "JavaLib/JavaLib" + from: "https://github.com/JavaJava/JavaLib" + version: 10.3.4 +tests: + - name: "main_tests" + depends: + - name: "core" + - name: "something" + command: "./main_tests" + - name: "functional_tests" + depends: + - name: "sys_contract" + - name: "Catch2/Catch2" + from: "catchorg/Catch2" + command: "./functional_tests -verbose=0" diff --git a/docs/sample2.yaml b/docs/sample2.yaml new file mode 100644 index 0000000..96aa717 --- /dev/null +++ b/docs/sample2.yaml @@ -0,0 +1,35 @@ +project: "antelope_system_contract" +version: "1.0.0" + +libraries: +apps: + - name: "sys_contract" + lang: "C++" + depends: + - name: "core" + - name: "something" + - name: "fast_math/math" + from: "https://github.com/larryk85/fast_math" + version: 3.0.1 + - name: "SomeLibrary/lib" + from: "AntelopeIO/SomeLibrary" + version: 1.0.2 + - name: "other" + #lang: "https://github.com/JavaJava/Java" + lang: "Java" + depends: + - name: "JavaLib/JavaLib" + from: "https://github.com/JavaJava/JavaLib" + version: 10.3.4 +tests: + - name: "main_tests" + depends: + - name: "core" + - name: "something" + command: "./main_tests" + - name: "functional_tests" + depends: + - name: "sys_contract" + - name: "Catch2/Catch2" + from: "catchorg/Catch2" + command: "./functional_tests -verbose=0" diff --git a/docs/sample3.yaml b/docs/sample3.yaml new file mode 100644 index 0000000..a1c4d5b --- /dev/null +++ b/docs/sample3.yaml @@ -0,0 +1,33 @@ +project: "antelope_system_contract" +version: "1.0.0" + +libraries: + - name: "core" + lang: "C++" + options: "-O0" + - name: "something" + lang: "C" + options: "-fno-something" +apps: + - name: "sys_contract" + lang: "C++" + depends: + - name: "other" + #lang: "https://github.com/JavaJava/Java" + lang: "Java" + depends: + - name: "JavaLib/JavaLib" + from: "https://github.com/JavaJava/JavaLib" + version: 10.3.4 +tests: + - name: "main_tests" + depends: + - name: "core" + - name: "something" + command: "./main_tests" + - name: "functional_tests" + depends: + - name: "sys_contract" + - name: "Catch2/Catch2" + from: "catchorg/Catch2" + command: "./functional_tests -verbose=0" diff --git a/project/CMakeLists.txt b/project/CMakeLists.txt new file mode 100644 index 0000000..b5ec209 --- /dev/null +++ b/project/CMakeLists.txt @@ -0,0 +1,28 @@ +set( target_name antler-project ) + +# I know. But it's appropriate to glob for now. +file(GLOB_RECURSE src_cpp ./src/*.cpp) +file(GLOB_RECURSE src_h ./src/*.h ./src/*.hpp) +file(GLOB inc_h ./antler/project/*.h ./antler/project/*.hpp ./antler/project/*.ipp) + +add_library(${target_name} ${src_cpp} ${src_h} ${inc_h}) +set_property(TARGET ${target_name} PROPERTY CXX_STANDARD 20) +target_compile_definitions(${target_name} PRIVATE + BUILDING_AP_PROJ +) +target_link_libraries(${target_name} PUBLIC + ryml c4core antler_common +) +target_include_directories(${target_name} PRIVATE + src +) +target_include_directories(${target_name} PUBLIC + . +) + + +# Install commands +install( + FILES ${inc_h} + DESTINATION antler/project COMPONENT Development +) diff --git a/project/antler/project/dependency.h b/project/antler/project/dependency.h new file mode 100644 index 0000000..882827f --- /dev/null +++ b/project/antler/project/dependency.h @@ -0,0 +1,91 @@ +#ifndef antler_project_dependency_h +#define antler_project_dependency_h + +#include +#include +#include +#include +#include +#include // std::pair +#include + + +namespace antler { +namespace project { + +class dependency { +public: + using list_t = std::vector; + + using patch_list_t = std::vector; + +public: + // use default constructors, copy and move constructors and assignments + + + /// Sets the internal values regardless of the validity. + /// @note Check that validate_location() returns true before setting these values. + void set(std::string_view name, std::string_view loc, std::string_view tag, std::string_view rel, std::string_view hash); + + + std::string_view name() const noexcept; + void name(std::string_view s) noexcept; + + /// @return the from location of this dependency. + std::string_view location() const noexcept; + void location(std::string_view s) noexcept; + + /// @return true if location ends in an archive format (e.g. ".tar.gz", ".tgz", etc") + bool is_archive() const noexcept; + + /// @return true if version is empty + bool empty_version() const noexcept; + + /// Sets github tag/commit hash. + std::string_view tag() const noexcept; + void tag(std::string_view s) noexcept; + + /// Sets github release version. + std::string_view release() const noexcept; + void release(std::string_view s) noexcept; + + + /// hash - only valid for archive and release. + std::string_view hash() const noexcept; + void hash(std::string_view s) noexcept; + + /// patch list + const patch_list_t& patch_files() const noexcept; + void patch_add(const std::filesystem::path& path) noexcept; + void patch_remove(const std::filesystem::path& path) noexcept; + + + /// @todo implement this function! + static bool validate_location(std::string_view s); + /// @param loc The from/location field of a dependency. Empty is valid. + /// @param tag The tag field of a dependency. Empty is valid. + /// @param rel The rel field of a dependency. Empty is valid. + /// @param hash The hash field of a dependency. Empty is valid. + /// @return true indicates the values passed in are a valid combination. + static bool validate_location(std::string_view loc, std::string_view tag, std::string_view rel, std::string_view hash, + std::ostream& os=std::cerr); + + +private: + std::string m_name; + std::string m_loc; ///< often a url? + std::string m_tag_or_commit; ///< github tag or commit hash. Always prefer a commit hash. + std::string m_rel; ///< github release version. Not valid with tag_or_commit. + std::string m_hash; ///< valid when m_loc is an archive (including github release version). + patch_list_t m_patchfiles; +}; + + +} // namespace project +} // namespace antler + + + + + +#endif diff --git a/project/antler/project/language.h b/project/antler/project/language.h new file mode 100644 index 0000000..50dfaa8 --- /dev/null +++ b/project/antler/project/language.h @@ -0,0 +1,36 @@ +#ifndef antler_project_language_h +#define antler_project_language_h + +#include +#include +#include + +namespace antler { +namespace project { + +enum class language { + none, + c, + cpp, + java, +}; + +language to_language(std::string_view s); + +std::string to_string(language e); + +extern const char* language_literals[]; + +} // namespace project +} // namespace antler + +std::ostream& operator<<(std::ostream& os, const antler::project::language& e); +std::istream& operator>>(std::istream& is, antler::project::language& e); + +namespace std { +inline std::string to_string(antler::project::language e) { return antler::project::to_string(e); }; +} // namespace std + + + +#endif diff --git a/project/antler/project/location.h b/project/antler/project/location.h new file mode 100644 index 0000000..3355341 --- /dev/null +++ b/project/antler/project/location.h @@ -0,0 +1,21 @@ +#ifndef antler_project_location_h +#define antler_project_location_h + +#include + +namespace antler { +namespace project { +namespace location { + + +bool is_archive(std::string_view s); +bool is_github_repo(std::string_view s); +bool is_github_archive(std::string_view s); +bool is_local_file(std::string_view s); +bool is_org_repo_shorthand(std::string_view s); + +} // namespace location +} // namespace project +} // namespace antler + +#endif diff --git a/project/antler/project/object.h b/project/antler/project/object.h new file mode 100644 index 0000000..21cac78 --- /dev/null +++ b/project/antler/project/object.h @@ -0,0 +1,97 @@ +#ifndef antler_project_object_h +#define antler_project_object_h + +#include +#include +#include +#include + +#include +#include + +namespace antler { +namespace project { + +class object { +public: + enum type_t { + none, + app, + lib, + test, + any, // any/all + }; + using list_t = std::vector; +public: + // use default constructors, copy and move constructors and assignments + object(type_t ot); + + /// Object constructor for app and lib types. + object(type_t ot, std::string_view name, antler::project::language lang, std::string_view opts); + /// Object constructor for test type. + object(std::string_view test_name, std::string_view command); + + + /// @return The type of this object. + type_t type() const noexcept; + + /// @return The project name. + std::string_view name() const noexcept; + /// Set the object name. + void name(std::string_view s) noexcept; + + + /// @return Current language. + antler::project::language language() const noexcept; + /// Replace any existing language info with the new value. + /// @param lang The new language value to store. + void language(antler::project::language lang) noexcept; + + /// @return Current options. + std::string_view options() const noexcept; + /// Replace any existing options with the new value. + /// @param options The new options to store. + void options(std::string_view options) noexcept; + + std::string_view command() const noexcept; + void command(std::string_view s) noexcept; + + + /// Update or insert a dependency. + /// @param dep The dependency to upsert. + void upsert_dependency(antler::project::dependency&& dep) noexcept; + /// Remove dependency if it exists. + /// @return true if the dependency was found and removed; otherwise, false (i.e. dependency does not exist) + bool remove_dependency(std::string_view name) noexcept; + /// @return The dependency list. + const antler::project::dependency::list_t& dependencies() const noexcept; + /// Search the lists to see if a dependency exists. + /// @param name The dependency name to search for. + bool dependency_exists(std::string_view name) const noexcept; + /// Return the dependency with the matching name. + /// @param name The name to search for in the dependency list. + /// @return optional with a copy of the dependency. + std::optional dependency(std::string_view name); + +private: + type_t m_type = none; + std::string m_name; + antler::project::dependency::list_t m_dependencies; + + // app, lib: + antler::project::language m_language = language::none; + std::string m_options; + + // test: + std::string m_command; +}; + + +} // namespace project +} // namespace antler + +std::ostream& operator<<(std::ostream& os, const antler::project::object::type_t& e); +std::istream& operator>>(std::istream& is, antler::project::object::type_t& e); + + +#endif diff --git a/project/antler/project/project.h b/project/antler/project/project.h new file mode 100644 index 0000000..464b949 --- /dev/null +++ b/project/antler/project/project.h @@ -0,0 +1,134 @@ +#ifndef antler_project_project_h +#define antler_project_project_h + +#include +#include +#include + +#include + + +namespace antler { +namespace project { + +class project { +public: + // + enum class pop { + force_replace, + honor_deltas, + // merge_deltas, + }; + +public: + + + + // parse a project from a project.yml + + // constructors + project(); + //project(const char* filename); + //project(const std::filesystem::path& filename); + + + std::string_view name() const noexcept; + void name(std::string_view s) noexcept; + + std::filesystem::path path() const noexcept; + void path(const std::filesystem::path& path) noexcept; + + antler::project::version version() const noexcept; + void version(const antler::project::version& ver) noexcept; + + + bool remove(std::string_view name, object::type_t type) noexcept; + + void upsert(object&& obj) noexcept; + void upsert_app(object&& app) noexcept; + void upsert_lib(object&& lib) noexcept; + void upsert_test(object&& test) noexcept; + + /// Search the lists to see if an object exists. + /// @param name The object name to search for. + /// @param type Limit the search to a single type. + /// @return true if an object with the provided name exists in the indicated list. + bool object_exists(std::string_view name, object::type_t type=object::type_t::any) const noexcept; + /// Return the first object with the matching name where search order is apps, libs, tests. + /// @param name The name to search for in the object lists. + /// @return optional with a copy of the object. + std::optional object(std::string_view name) const noexcept; + + const antler::project::object::list_t& apps() const noexcept; + const antler::project::object::list_t& libs() const noexcept; + const antler::project::object::list_t& tests() const noexcept; + antler::project::object::list_t all_objects() const noexcept; + + /// Validate the project. + bool is_valid(std::ostream& error_stream=std::cerr); + + + + /// Print the yaml object to a stream. + /// @param os The ostream to print to. + void print(std::ostream& os) const noexcept; + /// @return yaml string representation of this object. + std::string to_yaml() const noexcept; + + /// Write the file to disk. + /// @note path() must be set. + /// @param error_stream The stream to print failure reports to. + /// @return true for success; false for failure. + bool sync(std::ostream& error_stream=std::cerr) noexcept; + + /// Populate the directory by generating files. + /// @param action_type The type of population action to perform. + /// @param error_stream The stream to print failure reports to. + /// @return true for success; false for failure. + bool populate(pop action_type, std::ostream& error_stream=std::cerr) noexcept; + + + /// Factory function. + /// @note The returned project may not be valid. The only guarantee is that parsing did not fail. + /// @note see is_valid() to test validity. + /// @param path The location of the project.yaml file or the path containing it. + /// @param error_stream The stream to print failure reports to. + /// @return std::optional containing a project if parsing succeeded. + static std::optional parse(const std::filesystem::path& path, std::ostream& error_stream=std::cerr); + + /// Initialize the directories + /// @param path The location of the project.yaml file or the path containing it. + /// @param expect_empty This boolean describes behavior when paths preexist: + /// when true, any existing path - excluding the root - will cause an immediate false return; + /// when false, only failures to create will generate a false return. + /// @param error_stream The stream to print failure reports to. + /// @return true for success; false indidates failure. + static bool init_dirs(const std::filesystem::path& path, bool expect_empty=true, std::ostream& error_stream=std::cerr) noexcept; + + /// Search this and directories above for `project.yaml` file. + /// @note if path extension is `.yaml` no directory search is performed, instead return value indicating existence of path a regular file. + /// @param path This is the search path to begin with; if the project file was found, it is updated to the path to that file. + /// @return true if the project file was found and is a regular file; otherwise, false. + static bool update_path(std::filesystem::path& path) noexcept; + + /// Print the pop enum. + static void print(std::ostream& os, pop e) noexcept; + +private: + std::filesystem::path m_path; // path to the project.yaml file + std::string m_name; + antler::project::version m_ver; + object::list_t m_apps; + object::list_t m_libs; + object::list_t m_tests; +}; + + + +} // namespace project +} // namespace antler + +inline std::ostream& operator<<(std::ostream& os, const antler::project::project& o) { o.print(os); return os; } +inline std::ostream& operator<<(std::ostream& os, const antler::project::project::pop& e) { antler::project::project::print(os,e); return os; } + +#endif diff --git a/project/antler/project/semver.h b/project/antler/project/semver.h new file mode 100644 index 0000000..78f0f43 --- /dev/null +++ b/project/antler/project/semver.h @@ -0,0 +1,70 @@ +#ifndef antler_project_semver_h +#define antler_project_semver_h + +#include +#include + +#include + +#include +#include + + +namespace antler { +namespace project { + +class semver { +public: + using self = semver; + using value_type = unsigned; + + + semver(value_type x=0, value_type y=0, value_type z=0, std::string_view pre_release="", std::string_view build="") noexcept; + + // comparison operators + bool operator==(const self& rhs) const noexcept; + bool operator!=(const self& rhs) const noexcept; + bool operator<(const self& rhs) const noexcept; + bool operator<=(const self& rhs) const noexcept; + bool operator>(const self& rhs) const noexcept; + bool operator>=(const self& rhs) const noexcept; + + + void clear() noexcept; + + cmp_result compare(const self& rhs) const noexcept; + + std::string string() const noexcept; + void print(std::ostream& os) const noexcept; + + + /// Parse a semver from a given string. + static std::optional parse(std::string_view s) noexcept; + + +private: + /// compare prerelease according to rule 12. + static cmp_result compare_p_rule12(std::string_view lhs, std::string_view rhs) noexcept; + /// compare build according to rule 12. + static cmp_result compare_b_rule12(std::string_view lhs, std::string_view rhs) noexcept; + /// @return a comparison of lhs and rhs according to semver rule 12. lhs and rhs must both be EITHER pre-release or build and + /// both must be populated. + static cmp_result compare_pb_rule12(std::string_view lhs, std::string_view rhs) noexcept; + /// @return true indivcates valid pre-release or build string; false otherwise. + static bool validate_pb_rule10or11(std::string_view lhs) noexcept; + +private: + std::array m_xyz; + std::string m_pre; // pre-release + std::string m_build; // build number +}; + + +} // namespace project +} // namespace antler + + +inline std::ostream& operator<<(std::ostream& os, const antler::project::semver& o) { o.print(os); return os; } + + +#endif diff --git a/project/antler/project/so_support.h b/project/antler/project/so_support.h new file mode 100644 index 0000000..dd519ec --- /dev/null +++ b/project/antler/project/so_support.h @@ -0,0 +1,22 @@ +#ifndef project_parser_so_support_h +#define project_parser_so_support_h + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +// Support for shared libs. +#if defined(BUILDING_AP_PROJ) +# define AP_PROJ_API SO_EXPORT +#else +# define AP_PROJ_API SO_IMPORT +#endif + + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif diff --git a/project/antler/project/version.h b/project/antler/project/version.h new file mode 100644 index 0000000..cf37de4 --- /dev/null +++ b/project/antler/project/version.h @@ -0,0 +1,88 @@ +#ifndef antler_project_version_h +#define antler_project_version_h + +#include + +#include +#include +#include +#include +#include + + + +/// For semver requirements see: https://semver.org/spec/v2.0.0-rc.1.html +/// +/// @TODO - We need to improve pre-release as it's not currently calculated correctly. +/// See https://semver.org/spec/v2.0.0-rc.1.html#spec-item-10 +/// +/// @TODO - We need to add build number. +/// See https://semver.org/spec/v2.0.0-rc.1.html#spec-item-11 +/// +/// @TODO convert semver to struct { array; string pre_release; string build; } +/// See https://semver.org/spec/v2.0.0-rc.1.html#spec-item-12 for precedence. +/// + + +namespace antler { +namespace project { + +class version { +public: + using self = version; + + version(); + version(std::string_view ver); + version(const semver& sv); + version(const self& rhs); + + version& operator=(std::string_view ver); + version& operator=(const semver& sv); + version& operator=(const self& rhs); + + /// Clear any values. + void clear() noexcept; + + bool empty() const noexcept; + std::string_view raw() const noexcept; + + bool is_semver() const noexcept; + /// @return The version in semver_t format. If is_semver() would return false, this value is invalid. + operator semver() const noexcept; + + bool operator==(const self& rhs) const noexcept; + bool operator!=(const self& rhs) const noexcept; + bool operator<(const self& rhs) const noexcept; + bool operator<=(const self& rhs) const noexcept; + bool operator>(const self& rhs) const noexcept; + bool operator>=(const self& rhs) const noexcept; + +private: + enum class cmp { + eq, + lt, + gt, + }; + + static cmp raw_compare(std::string_view l_in, std::string_view r_in) noexcept; + cmp compare(const self& rhs) const noexcept; + + + void load(std::string_view s); + void load(const semver& sv); + +private: + std::string m_raw; + std::unique_ptr m_semver; +}; + + +} // namespace project +} // namespace antler + +inline std::ostream& operator<<(std::ostream& os, const antler::project::version& o) { os << o.raw(); return os; } +//std::istream& operator>>(std::istream& is, antler::project::object::version& e); + + + +#endif diff --git a/project/antler/project/version_compare.h b/project/antler/project/version_compare.h new file mode 100644 index 0000000..e2f276f --- /dev/null +++ b/project/antler/project/version_compare.h @@ -0,0 +1,29 @@ +#ifndef antler_project_version_compare_h +#define antler_project_version_compare_h + +#include +#include + + +namespace antler { +namespace project { + + +enum class cmp_result { + eq, + lt, + gt, +}; +void print(std::ostream& os, cmp_result e) noexcept; + +/// compare lhs and rhs by splitting on '.' and comparing results. This is a simple compare and does not consider semver presidence. +/// @return `lt` if lhs < rhs; `gt` if lhs > rhs; `eq` if lhs == rhs. +cmp_result raw_compare(std::string_view lhs, std::string_view rhs) noexcept; + + +} // namespace project +} // namespace antler + +inline std::ostream& operator<<(std::ostream& os, const antler::project::cmp_result& e) { antler::project::print(os,e); return os; } + +#endif diff --git a/project/antler/project/version_constraint.h b/project/antler/project/version_constraint.h new file mode 100644 index 0000000..c004c0a --- /dev/null +++ b/project/antler/project/version_constraint.h @@ -0,0 +1,64 @@ +#ifndef antler_project_version_constraint_h +#define antler_project_version_constraint_h + +#include +#include +#include + + +namespace antler { +namespace project { + +class version_constraint { +public: + using self = version_constraint; + + version_constraint(); + version_constraint(std::string_view ver); + + self& operator=(std::string_view ver); + + void clear(); + + bool is_unique() const noexcept; + + bool empty() const noexcept; + std::string_view raw() const noexcept; + + /// @note if empty() would return true, this function will ALWAYS return true. + /// @return true if ver met this constraint; false, otherwise. + bool test(const version& ver) const noexcept; + + void print(std::ostream& os) const noexcept; + +private: + + void load(std::string_view s, std::ostream& os=std::cerr); + + + std::string m_raw; + + enum class bounds_inclusivity { + none, + lower, + upper, + both, + unique, + }; + + struct constraint { + version lower_bound; + version upper_bound; // Unset *if* inclusivity is `unique`. + bounds_inclusivity inclusivity; + }; + std::vector m_constraints; +}; + + +} // namespace project +} // namespace antler + +inline std::ostream& operator<<(std::ostream& os, const antler::project::version_constraint& o) { o.print(os); return os; } +//std::istream& operator>>(std::istream& is, antler::project::object::version& e); + +#endif diff --git a/project/src/cmake.cpp b/project/src/cmake.cpp new file mode 100644 index 0000000..79b48b7 --- /dev/null +++ b/project/src/cmake.cpp @@ -0,0 +1,52 @@ +#include +#include + +namespace antler { +namespace cmake { + +std::string add_subdirectory(const std::filesystem::path path) noexcept { + //return std::format("add_subdirectory( {} )\n", path.string()); + std::ostringstream ss; + ss << "add_subdirectory( " << path << " )\n"; + return ss.str(); +} + + +std::string minimum(unsigned major, unsigned minor, unsigned patch) noexcept { + /* + if(!patch) { + if(!minor) + return std::format("cmake_minimum_required( {} )\n", major); + return std::format("cmake_minimum_required( {}.{} )\n", major,minor); + } + return std::format("cmake_minimum_required( {}.{}.{} )\n", major,minor,patch); + */ + + std::ostringstream ss; + ss << "cmake_minimum_required( " << major; + if(patch) + ss << "." << minor << "." << patch; + else if(minor) + ss << "." << minor; + ss << " )\n"; + + return ss.str(); +} + +std::string project(std::string_view proj_name) noexcept { + //return std::format("project( \"{}\" )\n", proj_name); + std::ostringstream ss; + ss << "project( \"" << proj_name << "\" )\n"; + return ss.str(); +} + + +std::string project(std::string_view proj_name, const project::semver& ver) noexcept { + //return std::format("project( \"{}\" VERSION {} )\n", proj_name, ver.string()); + std::ostringstream ss; + ss << "project( \"" << proj_name << "\" VERSION " << ver << " )\n"; + return ss.str(); +} + +} // namespace cmake +} // namespace antler diff --git a/project/src/cmake.h b/project/src/cmake.h new file mode 100644 index 0000000..11540a2 --- /dev/null +++ b/project/src/cmake.h @@ -0,0 +1,25 @@ +#ifndef antler_cmake_h +#define antler_cmake_h + +#include + +#include // path + + +namespace antler { +namespace cmake { + +/// @return the cmake_minimum string with trailing newline. +std::string minimum(unsigned major, unsigned minor=0, unsigned patch=0) noexcept; + +std::string add_subdirectory(const std::filesystem::path path) noexcept; + +/// @return A string including the project name: project("") +std::string project(std::string_view proj_name) noexcept; +/// @return A string including the project name: project("" VERSION ) +std::string project(std::string_view proj_name, const project::semver& ver) noexcept; + +} // namespace cmake +} // namespace antler + +#endif diff --git a/project/src/dependency.cpp b/project/src/dependency.cpp new file mode 100644 index 0000000..a09a9d0 --- /dev/null +++ b/project/src/dependency.cpp @@ -0,0 +1,201 @@ +#include +#include + +#include // std::sort, std::find() + + +namespace { + +inline bool is_valid_hash(std::string_view s, size_t byte_count=32) noexcept { + if(s.size() != byte_count) + return false; + for(auto a : s) { + if( !(a >= '0' && a <= '9') + && !(a >= 'a' && a <= 'f') + && !(a >= 'A' && a <= 'F') ) { + return false; + } + } + return true; +} + + +} // anonymous namespace + + +namespace antler { +namespace project { + + +//--- constructors/destructor ------------------------------------------------------------------------------------------ + + + +//--- alphabetic -------------------------------------------------------------------------------------------------------- + +bool dependency::empty_version() const noexcept { + return m_tag_or_commit.empty() && m_rel.empty(); +} + + +std::string_view dependency::hash() const noexcept { + return m_hash; +} + + +void dependency::hash(std::string_view s) noexcept { + m_hash = s; +} + + +bool dependency::is_archive() const noexcept { + return location::is_archive(m_loc); +} + + +std::string_view dependency::location() const noexcept { + return m_loc; +} + + +void dependency::location(std::string_view s) noexcept { + m_loc = s; +} + + +std::string_view dependency::name() const noexcept { + return m_name; +} + + +void dependency::name(std::string_view s) noexcept { + m_name = s; +} + + +void dependency::patch_add(const std::filesystem::path& path) noexcept { + // Only add if it doesn't already exist. + auto i = std::find(m_patchfiles.begin(), m_patchfiles.end(), path); + if( i != m_patchfiles.end() ) + return; + m_patchfiles.push_back(path); + std::sort(m_patchfiles.begin(), m_patchfiles.end()); // <-- this could be optimized with a binary search... +} + + +const dependency::patch_list_t& dependency::patch_files() const noexcept { + return m_patchfiles; +} + + +void dependency::patch_remove(const std::filesystem::path& path) noexcept { + + auto i = std::find(m_patchfiles.begin(), m_patchfiles.end(), path); + if( i != m_patchfiles.end() ) + m_patchfiles.erase(i); +} + + +std::string_view dependency::release() const noexcept { + return m_rel; +} + + +void dependency::release(std::string_view s) noexcept { + m_rel = s; +} + + +void dependency::set(std::string_view name, std::string_view loc, std::string_view tag, std::string_view rel, std::string_view hash) { + + m_name = name; + + m_loc = loc; + + m_tag_or_commit = tag; + m_rel = rel; + m_hash = hash; + m_patchfiles.clear(); + + + if(!m_tag_or_commit.empty() && !m_rel.empty()) { + std::cerr << "Unexpectedly have tag AND release. "; + if(is_valid_hash(m_tag_or_commit)) { + std::cerr << "Discarding release info.\n"; + m_rel.clear(); + } + else { + std::cerr << "Discarding tag info.\n"; + m_tag_or_commit.clear(); + } + } +} + + +std::string_view dependency::tag() const noexcept { + return m_tag_or_commit; +} + + +void dependency::tag(std::string_view s) noexcept { + m_tag_or_commit = s; +} + + +bool dependency::validate_location(std::string_view s) { + + return + location::is_archive(s) + || location::is_github_repo(s) + || location::is_org_repo_shorthand(s) + ; +} + + +bool dependency::validate_location(std::string_view loc, std::string_view tag, std::string_view rel, std::string_view hash, std::ostream& os) { + + // If location is empty, everything else should be too. + if(loc.empty()) { + if(tag.empty() && rel.empty() && hash.empty()) + return true; + os << "If location is empty, then:"; + if(!tag.empty()) + os << " tag"; + if(!rel.empty()) + os << " release"; + if(!hash.empty()) + os << " hash"; + os << " must also be empty."; + return false; + } + + if(!tag.empty()) { + if(!rel.empty()) { + os << "release AND tag/commit flags are not valid at the same time for location."; + return false; + } + if(!hash.empty()) { + os << "hash AND tag/commit flags are not valid at the same time for location."; + return false; + } + } + + if(location::is_archive(loc)) { + if(hash.empty()) + os << "Warning: archive locations should have a SHA256 hash."; + } + else if(location::is_github_repo(loc) || location::is_org_repo_shorthand(loc)) { + if(rel.empty() && tag.empty()) + os << "Warning: github locations should have either a tag/commit or release field."; + } + else { + os << "Unknown location type."; + return false; + } + + return true; +} + + +} // namespace project +} // namespace antler diff --git a/project/src/key.cpp b/project/src/key.cpp new file mode 100644 index 0000000..a3dc876 --- /dev/null +++ b/project/src/key.cpp @@ -0,0 +1,84 @@ +#include +#include + +#define WORD_CASE_OF \ + CASE_OF( none, "none" ) \ + \ + CASE_OF( apps, "apps" ) \ + CASE_OF( command, "command" ) \ + CASE_OF( depends, "depends" ) \ + CASE_OF( from, "from" ) \ + CASE_OF( hash, "hash" ) \ + CASE_OF( lang, "lang" ) \ + CASE_OF( libs, "libraries" ) \ + CASE_OF( name, "name" ) \ + CASE_OF( options, "options" ) \ + CASE_OF( patch, "patch" ) \ + CASE_OF( project, "project" ) \ + CASE_OF( release, "release" ) \ + CASE_OF( tag, "tag" ) \ + CASE_OF( tests, "tests" ) \ + CASE_OF( version, "version" ) \ + /* end WORD_CASE_OF */ + + + +namespace key { + + +const char* literals[] = { +#define CASE_OF(E,STR) STR, + WORD_CASE_OF +#undef CASE_OF +}; + + +std::string to_string(word e) { + + switch(e) { +#define CASE_OF(E,STR) case key::word::E: return STR; + WORD_CASE_OF + ; +#undef CASE_OF + } + + std::string s = "Unknown key::word ("; + s += std::to_string(unsigned(e)); + s += ")"; + return s; +} + + +//template +//word to_word(STRING_TYPE s) { +word to_word(std::string_view s) { + +#define CASE_OF(E, STR) if( s == STR) return key::word::E; + WORD_CASE_OF + ; +#undef CASE_OF + + return key::word::none; +} + + + + +} // namespace key + + +std::ostream& operator<<(std::ostream& os, const key::word& e) { + + switch(e) { +#define CASE_OF(E,STR) case key::word::E: os << STR; return os; + WORD_CASE_OF + ; +#undef CASE_OF + } + + os << "Unknown key::word (" << unsigned(e) << ")"; + return os; +} + + +#undef WORD_CASE_OF diff --git a/project/src/key.h b/project/src/key.h new file mode 100644 index 0000000..c1ef710 --- /dev/null +++ b/project/src/key.h @@ -0,0 +1,50 @@ +#ifndef src_key_h +#define src_key_h + +#include +#include +#include +#include + +namespace key { + + +enum class word { + none, + + apps, + command, + depends, + from, + hash, // Deps: indicates the SHA256 hash for an archive. Valid for github release and archive locations. + lang, + libs, + name, + options, + patch, + project, + release, // Deps: this indicates a github version to download. + tag, // Deps: indicates the github tag to clone. + tests, + version, // Deps: version is a synonym for release. +}; + +//template +//word to_word(STRING_TYPE s); +word to_word(std::string_view s); + +std::string to_string(word e); + +extern const char* literals[]; + +} // namespace key + + +std::ostream& operator<<(std::ostream& os, const key::word& e); + +namespace std { +inline std::string to_string(key::word e) { return key::to_string(e); }; +} // namespace std + + +#endif diff --git a/project/src/langague.cpp b/project/src/langague.cpp new file mode 100644 index 0000000..410e3c4 --- /dev/null +++ b/project/src/langague.cpp @@ -0,0 +1,84 @@ +#include + +#define LANGUAGE_CASE_OF \ + CASE_OF(none, "none") \ + \ + CASE_OF(c, "c") \ + CASE_OF(cpp, "cpp") \ + CASE_OF(java, "java") \ + /* end LANGUAGE_CASE_OF */ + + + +namespace antler { +namespace project { + + +const char* language_literals[] { +#define CASE_OF(E,STR) STR, + LANGUAGE_CASE_OF +#undef CASE_OF +}; + +language to_language(std::string_view s) { + +#define CASE_OF(X,Y) if( s == Y) return antler::project::language::X; + LANGUAGE_CASE_OF + ; +#undef CASE_OF + + // This should be changed to have an internal function: + // Call it once, if it returns none, downcase `s` and call it again. + + // Some additional values: + if( s == "C++" || s == "CPP" || s == "c++" ) + return antler::project::language::cpp; + if( s == "C" ) + return antler::project::language::c; + if( s == "Java" || s == "JAVA" ) + return antler::project::language::java; + + return antler::project::language::none; +} + + +std::string to_string(language e) { + + switch(e) { +#define CASE_OF(X,Y) case antler::project::language::X: return Y; + LANGUAGE_CASE_OF; +#undef CASE_OF + } + + std::string s = "Unknown antler::project::language ("; + s += std::to_string(unsigned(e)); + s += ")"; + return s; +} + + +} // namespace project +} // namespace antler + + +std::ostream& operator<<(std::ostream& os, const antler::project::language& e) { + switch(e) { +#define CASE_OF(X,Y) case antler::project::language::X: { os << Y; return os; } + LANGUAGE_CASE_OF; +#undef CASE_OF + } + os << "Unknown antler::project::language (" << unsigned(e) << ")"; + return os; +} + + +std::istream& operator>>(std::istream& is, antler::project::language& e) { + + std::string temp; + if(is >> temp) + e = antler::project::to_language(temp); + else + // This might be an exceptional state and so maybe we should throw an exception? + e = antler::project::language::none; + return is; +} diff --git a/project/src/location.cpp b/project/src/location.cpp new file mode 100644 index 0000000..c6a5652 --- /dev/null +++ b/project/src/location.cpp @@ -0,0 +1,83 @@ +#include +#include +#include + +#include + +#include + + +namespace antler { +namespace project { +namespace location { + +bool is_archive(std::string_view s) { + + return + s.ends_with(".tar.gz") + || s.ends_with(".tgz") + || s.ends_with(".tar.bz2") + || s.ends_with(".tar.xz") + || s.ends_with(".tar.zst") + ; +} + + +bool is_github_archive(std::string_view s) { + + return s.starts_with("https://github.com") && is_archive(s); + +} + + +bool is_github_repo(std::string_view s) { + + return s.starts_with("https://github.com") && !is_archive(s); + +} + + +bool is_local_file(std::string_view s) { + + if(s.starts_with("https://github.com")) + return false; + + if(s.starts_with("file://")) + s = s.substr(7); + + std::error_code sec; + return std::filesystem::exists(s,sec); +} + + +bool is_org_repo_shorthand(std::string_view s) { + + auto splits = string::split(s, "/"); + if(splits.size() != 2) + return false; + + // Set ss to the command string. + std::ostringstream ss; + ss << "gh repo list " << splits[0] << " --json name"; + + // Call the command, test the result. + const auto result = system::exec(ss.str()); + if(!result) { + std::cerr << result.output << "\n"; + return false; + } + + // Reset/clear the stream and write the search string into it. + ss.str(""); // Reset the stream. + ss << "\"name\":\"" << splits[1] << "\""; + + // Search for the repo. + return result.output.find(ss.str()) != std::string::npos; +} + + + + +} // namespace location +} // namespace project +} // namespace antler diff --git a/project/src/object.cpp b/project/src/object.cpp new file mode 100644 index 0000000..e6d536a --- /dev/null +++ b/project/src/object.cpp @@ -0,0 +1,160 @@ +#include + +#include // find_if() + +namespace antler { +namespace project { + + +//--- constructors/detractor -------------------------------------------------------------------------------------------- + +object::object(type_t ot) + : m_type{ot} +{ +} + + +object::object(type_t ot, std::string_view name, antler::project::language lang, std::string_view opts) + : m_type{ot} + , m_name{name} + , m_language{lang} + , m_options{opts} +{ +} + + +object::object(std::string_view name, std::string_view command) + : m_type{object::test} + , m_name{name} + , m_command{command} +{ +} + + +//--- alphabetic -------------------------------------------------------------------------------------------------------- + + +std::string_view object::command() const noexcept { + return m_command; +} + + +void object::command(std::string_view s) noexcept { + m_command = s; +} + + +const antler::project::dependency::list_t& object::dependencies() const noexcept { + return m_dependencies; +} + + +std::optional object::dependency(std::string_view name) { + auto rv = std::find_if(m_dependencies.begin(), m_dependencies.end(), [name](const auto& d) { return d.name() == name; }); + if( rv != m_dependencies.end() ) + return *rv; + + return std::optional{}; +} + + +bool object::dependency_exists(std::string_view name) const noexcept { + return std::find_if(m_dependencies.begin(), m_dependencies.end(), [name](const auto& d) { return d.name() == name; }) != m_dependencies.end(); +} + + +antler::project::language object::language() const noexcept { + return m_language; +} + +void object::language(antler::project::language lang) noexcept { + m_language = lang; +} + + +std::string_view object::name() const noexcept { + return m_name; +} + + +void object::name(std::string_view s) noexcept { + m_name = s; +} + + +std::string_view object::options() const noexcept { + return m_options; +} + + +void object::options(std::string_view options) noexcept { + m_options = options; +} + + +bool object::remove_dependency(std::string_view name) noexcept { + // If possible, find a dependency with matching name and return it. + auto i = std::find_if(m_dependencies.begin(), m_dependencies.end(), [name](const antler::project::dependency& d) { return d.name() == name; } ); + if( i == m_dependencies.end() ) + return false; + m_dependencies.erase(i); + return true; +} + + +object::type_t object::type() const noexcept { + return m_type; +} + + +void object::upsert_dependency(antler::project::dependency&& dep) noexcept { + // If possible, find a dependency with matching name and reutrn it. + auto i = std::find_if(m_dependencies.begin(), m_dependencies.end(), [dep](const antler::project::dependency& d) { return d.name() == dep.name(); } ); + if( i == m_dependencies.end() ) + m_dependencies.emplace_back(dep); + else + *i = dep; +} + + +} // namespace project +} // namespace antler + + +//--- global operators ------------------------------------------------------------------------------------------ + + + +#define TYPE_T_CASE_OF \ + CASE_OF(none, "none") \ + CASE_OF(app, "app") \ + CASE_OF(lib, "lib") \ + CASE_OF(test, "test") \ + CASE_OF(any, "any") \ + /* end TYPE_T_CASE_OF */ + + + +std::ostream& operator<<(std::ostream& os, const antler::project::object::type_t& e) { + switch(e) { +#define CASE_OF(X,Y) case antler::project::object::type_t::X: { os << Y; return os; } + TYPE_T_CASE_OF; +#undef CASE_OF + } + os << "Unknown antler::project::object::type_t (" << unsigned(e) << ")"; + return os; +} + + +std::istream& operator>>(std::istream& is, antler::project::object::type_t& e) { + + std::string temp; + if(is >> temp) { +#define CASE_OF(X,Y) if( temp == Y) { e = antler::project::object::type_t::X; return is; } + TYPE_T_CASE_OF; +#undef CASE_OF + } + // This might be an exceptional state and so maybe we should throw an exception? + e = antler::project::object::type_t::none; + return is; +} diff --git a/project/src/project-is_valid.cpp b/project/src/project-is_valid.cpp new file mode 100644 index 0000000..28d5c29 --- /dev/null +++ b/project/src/project-is_valid.cpp @@ -0,0 +1,26 @@ +#include + + +#define TEST_POPULATED(X,Y) if(X.empty()) { os << Y << " is unpopulated.\n"; rv = false; } + +namespace antler { +namespace project { + +bool project::is_valid(std::ostream& os) { + + bool rv = true; + + // First, validate the members of this object. + TEST_POPULATED( m_path, "path" ); + TEST_POPULATED( m_name, "name" ); + TEST_POPULATED( m_ver, "version" ); + + // Now validate: apps, libs, and tests. + + + return rv; +} + + +} // namespace project +} // namespace antler diff --git a/project/src/project-parse.cpp b/project/src/project-parse.cpp new file mode 100644 index 0000000..6b5cfff --- /dev/null +++ b/project/src/project-parse.cpp @@ -0,0 +1,451 @@ +#include +#include + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wold-style-cast" + +#include +#include // to_substr(std::string) + +#pragma GCC diagnostic pop + + +#include +#include +#include + + + +namespace antler { +namespace project { + +namespace { + +/// Load a text file into a string. +/// +/// @noteThis should be optimized and stored somewhere. Something like this is a useful library function - when optimized... +/// +/// @param path Path to the file. +/// @param os ostream to write errors to. +/// @return An optional string that is populated with the file contents *if* the load was successful; otherwise, it's invalid for any error. +std::optional load(const std::filesystem::path& path, std::ostream& os) { + + using return_type = std::optional; + + std::error_code sec; + + // Sanity check and determine the file size. + if( !std::filesystem::exists(path,sec) ) { + os << "Path doesn't exist: " << path << "\n"; + return return_type(); + } + if( !std::filesystem::is_regular_file(path,sec) ) { + os << "Path must be regular file: " << path << "\n"; + return return_type(); + } + std::uintmax_t sz = std::filesystem::file_size(path,sec); + if(sz == static_cast(-1)) { + os << "Can't determine file size for: " << path << " with error " << sec << "\n"; + return return_type(); + } + + // Create a string of the appropriate size for loading. + std::string rv; + rv.reserve(sz); + + // And load it... + std::ifstream infile(path); + rv.append(std::istreambuf_iterator(infile), std::istreambuf_iterator()); + + return rv; +} + + +} // anonymous namespace + + +/// Parse the dependency portion of an antler-pack project file. Error are written to os. +/// @param node Reference to the node to be parsed. +/// @param os Stream for prinitng errors. +/// @return optional of dependency type. Dependency is populated on successful parse only. +std::optional parse_depends(const ryml::NodeRef& node, std::ostream& os) { + + using return_type = std::optional; + + dependency rv; + + // We assume node is a map. + + // For each element in this node. + for(auto i : node) { + // Sanity check. + if( !i.has_key()) { + os << "no key\n"; + continue; + } + + // Get the key as one of our enums for a switch. + key::word word = key::to_word(i.key()); + switch(word) { + + case key::word::name: { + // Sanity check before setting value. + if( !i.has_val() ) { + os << word << " tag in dependency list with no value.\n"; + return return_type(); + } + if( !rv.name().empty() ) { + os << "Duplicate " << word << " values in dependency list: " << i.val() << ", " << rv.name() << "\n"; + return return_type(); + } + rv.name(i.val()); + } break; + + case key::word::tag: { + // Sanity check before setting value. + if( !i.has_val() ) { + os << word << " tag in dependency list with no value.\n"; + return return_type(); + } + if( !rv.tag().empty() ) { + os << "Duplicate " << word << " values in dependency list: " << i.val() << ", " << rv.tag() << "\n"; + return return_type(); + } + rv.tag(i.val()); + } break; + + case key::word::release: + case key::word::version: { // Allow version to mean release. + // Sanity check before setting value. + if( !i.has_val() ) { + os << word << " tag in dependency list with no value.\n"; + return return_type(); + } + if( !rv.release().empty() ) { + os << "Duplicate " << word << " values in dependency list: " << i.val() << ", " << rv.release() << "\n"; + return return_type(); + } + rv.release(i.val()); + } break; + + case key::word::hash: { + // Sanity check before setting value. + if( !i.has_val() ) { + os << word << " tag in dependency list with no value.\n"; + return return_type(); + } + if( !rv.hash().empty() ) { + os << "Duplicate " << word << " values in dependency list: " << i.val() << ", " << rv.hash() << "\n"; + return return_type(); + } + rv.hash(i.val()); + } break; + + case key::word::from: { + // Sanity check before setting value. + if( !i.has_val() ) { + os << word << " tag in dependency list with no value.\n"; + return return_type(); + } + if( !rv.location().empty() ) { + os << "Duplicate " << word << " values in dependency list: " << i.val() << ", " << rv.location() << "\n"; + return return_type(); + } + if( !dependency::validate_location(i.val()) ) { + os << "Invalid location: " << i.val() << "\n"; + return return_type(); + } + rv.location(i.val()); + } break; + + case key::word::patch: { + // Get the patch file paths. + for(auto fn : i) { + // Sanity check. + if( !fn.has_val()) { + os << "no val\n"; + continue; + } + std::string_view temp = fn.val(); + rv.patch_add(temp); + } + } break; + + + case key::word::project: + case key::word::libs: + case key::word::apps: + case key::word::tests: + case key::word::lang: + case key::word::options: + case key::word::depends: + case key::word::command: { + os << "Unexpected tag in dependency list: " << word << "\n"; + return return_type(); + } + + case key::word::none: { + os << "Unknown tag in dependency list: " << i.key() << "\n"; + return return_type(); + } + } + } + + return rv; +} + + +/// Parse the object portion of an antler-pack project file. Error are written to os. +/// @param node Reference to the node to be parsed. +/// @param os Stream for prinitng errors. +/// @return optional of object type. Dependency is populated on successful parse only. +std::optional parse_object(const ryml::NodeRef& node, object::type_t type, std::ostream& os) { + + using return_type = std::optional; + + object rv(type); + + for(auto i : node) { + if( !i.has_key()) { + os << "no key\n"; + continue; + } + + // Get the key as one of our enums for a switch. + key::word word = key::to_word(i.key()); + switch(word) { + + case key::word::name: { + // Sanity check before setting value. + if( !i.has_val() ) { + os << "Name tag in " << type << " list with no value.\n"; + return return_type(); + } + if( !rv.name().empty() ) { + os << "Duplicate name values in " << type << " list: " << i.val() << "\n"; + return return_type(); + } + rv.name(i.val()); + } break; + + case key::word::lang: { + // Sanity check before setting value. + if( !i.has_val() ) { + os << word << " tag in " << type << " list with no value.\n"; + return return_type(); + } + auto lang = to_language(i.val()); + if(lang == language::none) { + os << "Invalid language tag in " << type << " list: " << i.val() << "\n"; + return return_type(); + } + if( rv.language() != language::none ) { + os << "Duplicate language values in " << type << " list: " << rv.language() << ", " << lang << "\n"; + return return_type(); + } + rv.language(lang); + } break; + + case key::word::options: { + // Sanity check before setting value. + if( !i.has_val() ) { + os << word << " tag in " << type << " list with no value.\n"; + return return_type(); + } + if(!rv.options().empty()) { + os << "Duplicate " << word << " values in " << type << " list: " << rv.options() << ", " << i.val() << "\n"; + return return_type(); + } + rv.options(i.val()); + } break; + + + case key::word::command: { + // Sanity check before setting value. + if( !i.has_val() ) { + os << word << " tag in " << type << " list with no value.\n"; + return return_type(); + } + if(!rv.command().empty()) { + os << "Duplicate " << word << " values in " << type << " list: " << rv.command() << ", " << i.val() << "\n"; + return return_type(); + } + rv.command(i.val()); + } break; + + + case key::word::depends: { + // sanity check + if( i.has_val() && !i.val().empty() ) { + os << "Unexpected value in " << word << " list: " << i.val() << "\n"; + return return_type(); + } + // Depends should be a map. For each element, parse out the dependency and store it. + for(auto j : i) { + auto optional_dep = parse_depends(j, os); + if(!optional_dep) + return return_type(); + if(rv.dependency_exists(optional_dep.value().name())) { + os << "Multiple dependencies with the same name in " << word << " list: " << optional_dep.value().name() << "\n"; + return return_type(); + } + rv.upsert_dependency(std::move(optional_dep.value())); + } + } break; + + + case key::word::apps: + case key::word::from: + case key::word::hash: + case key::word::libs: + case key::word::patch: + case key::word::project: + case key::word::release: + case key::word::tag: + case key::word::tests: + case key::word::version: { + os << "Unexpected tag in " << type << " list: " << word << "\n"; + return return_type(); + } + + case key::word::none: { + os << "Unknown tag in " << type << " list: " << i.key() << "\n"; + return return_type(); + } + + } + } + + return rv; +} + + +std::optional project::parse(const std::filesystem::path& path, std::ostream& os) { + + using return_type = std::optional; + + // Get file contents and store it in source. + std::string source; + { + auto temp = load(path,os); + if(!temp) + return return_type(); + source = temp.value(); + } + + // Parse source IN PLACE. So do NOT modify source after parsing! + ryml::Tree tree = ryml::parse_in_place(c4::to_substr(source)); + + // Create the project object and set it's source path. + project rv; + rv.path(path); + + // For each member of the tree... + for( const auto& i : tree.rootref() ) { + // Sanity check. + if( !i.has_key()) { + os << "Missing key in root.\n"; + return return_type{}; + } + + // Get the key as one of our enums for a switch. + key::word word = key::to_word(i.key()); + switch(word) { + + case key::word::project: { + // Sanity check before setting value. + if( !i.has_val() ) { + os << "Project tag at root level with no value.\n"; + return return_type(); + } + if( !rv.name().empty() ) { + os << "Multiple project tags at root level: " << rv.name() << ", " << i.val() << "\n"; + return return_type(); + } + rv.name(i.val()); + } break; + + case key::word::version: { + // Sanity check before setting value. + if( !i.has_val() ) { + os << "Version tag at root level with no value.\n"; + return return_type(); + } + if( !rv.version().empty() ) { + os << "Multiple version tags at root level: " << rv.version() << ", " << i.val() << "\n"; + return return_type(); + } + rv.version( antler::project::version(i.val()) ); + } break; + + case key::word::apps: + case key::word::libs: + case key::word::tests: { + // sanity check + if( i.has_val() && !i.val().empty() ) { + os << "Unexpected value in " << word << " list: " << i.val() << "\n"; + return return_type(); + } + + // Apps, libs, and tests are nearly the same. But they have a few small differences in parsing and also end up in + // different lists. So here we set the object type and a reference to the storage list. + // + // This allows us to write and maintain a single block of code to parse each list. + + // The list type. + const object::type_t ot = + (word == key::word::apps ? object::app : + (word == key::word::libs ? object::lib : object::test) ); + // A reference to the list we want to populate. + object::list_t& list = + (ot == object::app ? rv.m_apps : + (ot == object::lib ? rv.m_libs : rv.m_tests) ); + + // For each object in the list, call parse object. + for(auto node : i) { + auto optional_obj = parse_object(node, ot, os); + // sanity check before storing. + if(!optional_obj) + return return_type(); + if(rv.object_exists(optional_obj.value().name(),ot)) { + os << "Multiple object with the same name in " << word << " list: " << optional_obj.value().name() << "\n"; + return return_type(); + } + list.emplace_back(optional_obj.value()); + } + } break; + + + case key::word::command: + case key::word::depends: + case key::word::from: + case key::word::hash: + case key::word::lang: + case key::word::name: + case key::word::options: + case key::word::patch: + case key::word::release: + case key::word::tag: { + os << "Unexpected tag at root level: " << word << "\n"; + return return_type(); + } + + case key::word::none: { + os << "Unknown tag at root level: " << i.key() << "\n"; + return return_type(); + } + + } + } + + + // Validate here. + if( !rv.is_valid(os) ) + return return_type(); + + return rv; +} + + +} // namespace project +} // namespace antler diff --git a/project/src/project-populate.cpp b/project/src/project-populate.cpp new file mode 100644 index 0000000..21f4002 --- /dev/null +++ b/project/src/project-populate.cpp @@ -0,0 +1,130 @@ +#include +#include + +#include +#include + +namespace antler { +namespace project { + +namespace { // anonymous + +const std::filesystem::path cmake_lists{"CMakeLists.txt"}; +constexpr std::string_view magic1{"# This file was AUTOGENERATED and is maintained using antler-proj tools."}; // Intentionally missing newline. +constexpr std::string_view magic2{"# Modification or removal of the above line will cause antler-proj to skip this line."}; +constexpr std::string_view magic3{"# Any changes made to this file will be lost when any antler-proj tool updates this project."}; +const std::string magic_all{ std::string(magic1) + "\n" + std::string(magic2) + "\n" + std::string(magic3) + "\n" }; + +/// Test to see if a file has the magic maintenance string. +/// @return true if the file either does NOT exist OR contains the magic +bool has_magic(const std::filesystem::path& path, std::ostream& error_stream=std::cerr) { + + std::error_code sec; + + if(!std::filesystem::exists(path,sec)) + return true; + + // search path for magic1. + std::ifstream ifs(path); + if(!ifs.is_open()) { + error_stream << "Failed to open " << path << "\n"; + return false; + } + + for(std::array buffer; ifs.getline(buffer.data(), buffer.size()); /**/) { + // Sanity check size of search string against the buffer. + static_assert(magic1.size() < buffer.size(), "Buffer is to small to test for magic value."); + if(magic1 == buffer.data()) + return true; + } + + return false; +} + + +} // anonymous namespace + + +bool project::populate(pop action_type, std::ostream& error_stream) noexcept { + + bool force_replace = (action_type == pop::force_replace); + + // Sanity check: ensure path is valid. + if(m_path.empty()) { + error_stream << "Can not populate a project without a path.\n"; + return false; + } + + // Find the project path, and make sure the subdirs/project directory tree exists. + auto project_path = m_path.parent_path(); + if(!init_dirs(project_path, false, error_stream)) // expect_empty is `false`, it's okay if everthing exists. + return false; // But its not okay if the filesystem doesn't already exist AND can't be created. + + std::error_code sec; + + // Check to see if the top level cmake file needs to be created. + { + auto path=project_path/cmake_lists; + bool create = true; + if(!force_replace && std::filesystem::exists(path,sec) ) { + // Look to see if the header contains the magic, if it does we will not create the file. + create=has_magic(path); + } + if(create) { + std::ofstream ofs(path); + if(!ofs.good()) { + error_stream << "Can not open path for writing: << " << path << "\n"; + } + else { + try { + ofs + << magic_all + << "\n" + << cmake::minimum(3,11) + << "\n" + << (m_ver.is_semver() ? cmake::project(m_name) : cmake::project(m_name, static_cast(m_ver)) ) + << "\n" + << cmake::add_subdirectory("libs") + << cmake::add_subdirectory("apps") + << "\n" + << "option(BUILD_TESTS \"Build and run the tests.\" On)\n" + << "if(BUILD_TESTS)\n" + << " enable_testing()\n" + << " " << cmake::add_subdirectory("tests") + << "endif()\n" + ; + } + catch(std::exception& e) { + error_stream << "Error writing to " << path << ": " << e.what() << "\n"; + return false; + } + catch(...) { + error_stream << "Error writing to " << path << ": UNKNOWN\n"; + return false; + } + } + } + } + + + + // At each level we are going to want to create a CMakeLists.txt file and zero or more `.cmake` include files. + + // Each file includes a header indicating the user should not modify it. And if they do, changes will be lost unless they + // delete this line; however, if they delete the line, auto updates will no longer be possible. + + // Then we update everything. + + + + // Finally CMake the project. Ensure the output goes to a log file. + + + + + return false; +} + + +} // namespace project +} // namespace antler diff --git a/project/src/project-print.cpp b/project/src/project-print.cpp new file mode 100644 index 0000000..18deded --- /dev/null +++ b/project/src/project-print.cpp @@ -0,0 +1,240 @@ +#include +#include + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wold-style-cast" + +#include +#include // to_csubstr(std::string) + +#pragma GCC diagnostic pop + +#include + + +namespace { // anonymous + +/// Templates to work around ryml::csubstr data type. Note that csubstr is very similar to std::string_view. + +template +const c4::csubstr to_csubstr(T t) { + // The default function should probably not exist. It will ONLY work in liited situations. In other cases, it silently allows + // ryml to write garbage. + + t.force_this_template_function_to_generate_an_error(); // "disable" calls to this function. + std::stringstream ss; + ss << t; + return c4::to_csubstr(ss.str()); +} + +template<> +const c4::csubstr to_csubstr(std::string s) { + return c4::to_csubstr(s); +} + + +template<> +const c4::csubstr to_csubstr(std::string_view s) { + return c4::to_csubstr(s); +} + +// template<> +// const c4::csubstr to_csubstr(const std::string& s) { +// return c4::to_csubstr( s ); +//} + + +template<> +const c4::csubstr to_csubstr(const char* c) { + return c4::to_csubstr(c); +} + +template<> +const c4::csubstr to_csubstr(key::word e) { + return to_csubstr(key::literals[static_cast(e)]); +} + + +template<> +const c4::csubstr to_csubstr(antler::project::language e) { + return to_csubstr(antler::project::language_literals[static_cast(e)]); +} + +template +const c4::csubstr to_csubstr_insert(T t) { + std::stringstream ss; + ss << t; + return c4::to_csubstr(ss.str()); +} + +template<> +const c4::csubstr to_csubstr_insert(antler::project::version v) { + // This is a hack for now. + return c4::to_csubstr(v.raw()); +} +/*template<> +const c4::csubstr to_csubstr_insert(const antler::project::version& v) { + // This is a hack for now. + return c4::to_csubstr( v.raw() ); +} +*/ + +/* +const c4::csubstr literal(key::word e) { + return c4::to_csubstr(key::literals[static_cast(e)]); +} +*/ + +} // anonymous namespace + + + +namespace antler { +namespace project { + +void project::print(std::ostream& os) const noexcept { + + // Warning: nodes do not like non-literal values!!! + // Be very, very careful here or you'll end up with garbage and segfaults. + + + ryml::Tree tree; + + // We want a root node that's a map. + auto root = tree.rootref(); + root.change_type(c4::yml::MAP); + + // Store the project name and version + if (!m_name.empty()) + root[to_csubstr(key::word::project)] << to_csubstr(m_name); + + if (!m_ver.empty()) + root[to_csubstr(key::word::version)] << to_csubstr_insert(m_ver); + + // Maintain once. + // + // We are going to use the same code to write out the library, apps, and test sequences. To do this, we will create a list + // containing a reference/pointer to the project's lists and a list of the corresponding type. We iterate through each one. + + const std::vector obj_lists{ &m_libs, &m_apps, &m_tests }; + const std::vector list_type{ key::word::libs, key::word::apps, key::word::tests }; + + for (size_t i = 0; i < obj_lists.size(); ++i) { + const auto& obj_list = *obj_lists[i]; // convenience. + if (obj_list.empty()) // Don't add the list if it's empty. + continue; + + // Each of these lists is a yaml sequence, so create a sequence node with the correct key for the list type. + auto obj_node = (root[to_csubstr(list_type[i])] << ""); + obj_node.change_type(c4::yml::SEQ); + + // Iterate through the list of objects... + for (size_t j = 0; j < obj_list.size(); ++j) { + const auto& obj = obj_list[j]; // convenience + + // Create a map node to contain the key/value pairs. + auto map_node = obj_node[j] << ""; + map_node.change_type(c4::yml::MAP); + + // Add the elements. + + // name + if (!obj.name().empty()) + map_node[to_csubstr(key::word::name)] << to_csubstr(obj.name()); + // lang + if (obj.language() != language::none) + map_node[to_csubstr(key::word::lang)] << to_csubstr(obj.language()); + // options + if (!obj.options().empty()) + map_node[to_csubstr(key::word::options)] << to_csubstr(obj.options()); + + // depends - dependencies are also a list. + if (!obj.dependencies().empty()) { + // Make the sequence node with the correct key. + auto dep_node = (map_node[to_csubstr(key::word::depends)] << ""); + dep_node.change_type(c4::yml::SEQ); + + // Iterate over every element in the dependency list... + for (size_t k = 0; k < obj.dependencies().size(); ++k) { + const auto& dep = obj.dependencies()[k]; // convenience + + // Create a map node to contain the key/value pairs. + auto dep_map_node = dep_node[k] << ""; + dep_map_node.change_type(c4::yml::MAP); + + // Add the elements. + // name + if (!dep.name().empty()) + dep_map_node[to_csubstr(key::word::name)] << to_csubstr(dep.name()); + // location + if (!dep.location().empty()) + dep_map_node[to_csubstr(key::word::from)] << to_csubstr(dep.location()); + // tag or commit hash + if (!dep.tag().empty()) + dep_map_node[to_csubstr(key::word::tag)] << to_csubstr(dep.tag()); + // release + if (!dep.release().empty()) + dep_map_node[to_csubstr(key::word::release)] << to_csubstr(dep.release()); + // hash + if (!dep.hash().empty()) + dep_map_node[to_csubstr(key::word::hash)] << to_csubstr(dep.hash()); + + // Patch files. + const auto& patch_files = dep.patch_files(); + if (!patch_files.empty()) { + auto patch_node = dep_map_node[to_csubstr(key::word::patch)] << ""; + patch_node.change_type(c4::yml::SEQ); + for (size_t n = 0; n < patch_files.size(); ++n) + patch_node[n] << patch_files[n]; + } + } + } + // command + if (!obj.command().empty()) { + map_node[to_csubstr(key::word::command)] << to_csubstr(obj.command()); + } + } + } + + + + // Add a header. + os << "# `" << (!m_name.empty() ? m_name : "UNNAMED") << "` project file.\n" + << "# generated by antler-pack v" + << "TBD" + << "\n"; + + // If we know the source file name, then add it to the header. + if (!m_path.empty()) + os << "# source file: " << m_path.string() << "\n"; + + + os << "#\n" + << "# This file was auto-generated. Be aware antler-pack may discard added comments.\n" + << "\n\n"; + os << tree; +} + + + +#define CASEOF_POP_LIST \ + CASE_OF(force_replace) \ + CASE_OF(honor_deltas) \ + /* CASEOF_POP_LIST */ + +void project::print(std::ostream& os, pop e) noexcept { + + // clang-format off + switch (e) { +#define CASE_OF(X) case pop::X: {os << #X; return;} + CASEOF_POP_LIST; +#undef CASE_OF + } + // clang-format on + + os << "Unknown project::pop (" << unsigned(e) << ")"; +} + + +} // namespace project +} // namespace antler diff --git a/project/src/project.cpp b/project/src/project.cpp new file mode 100644 index 0000000..f08978b --- /dev/null +++ b/project/src/project.cpp @@ -0,0 +1,306 @@ +#include + +#include +#include +#include // find_if() +#include + +namespace antler { +namespace project { + + +//--- constructors/destrructor ------------------------------------------------------------------------------------------ + +project::project() = default; +/* +project::project(const char* filename) { + parse(filename); +} + + +project::project(const std::filesystem::path& filename) { + parse(filename); +} +*/ + +//--- alphabetic -------------------------------------------------------------------------------------------------------- + +object::list_t project::all_objects() const noexcept { + auto rv = m_apps; + rv.reserve(m_apps.size() + m_libs.size() + m_tests.size()); + for(auto a : m_libs) + rv.emplace_back(a); + for(auto a : m_tests) + rv.emplace_back(a); + return rv; +} + + +const object::list_t& project::apps() const noexcept { + return m_apps; +} + + +bool project::init_dirs(const std::filesystem::path& path, bool expect_empty, std::ostream& error_stream) noexcept { + + std::error_code sec; + + // Create the root directory. + std::filesystem::create_directories(path,sec); + if(sec) { + error_stream << path << " could not be created: " << sec << "\n"; + return false; + } + + if(expect_empty && !std::filesystem::is_empty(path,sec)) { + error_stream << path << " is NOT empty!\n"; + return false; + } + + // Create the directory structure. + { + const std::vector files = {"apps", "include", "ricardian", "libs", "tests" }; + for(const auto& fn : files) { + std::filesystem::create_directory(path/fn,sec); + if(sec) { + error_stream << (path/fn) << " could not be created: " << sec << "\n"; + return false; + } + } + } + return true; +} + + +const object::list_t& project::libs() const noexcept { + return m_libs; +} + + +std::string_view project::name() const noexcept { + return m_name; +} + + +void project::name(std::string_view s) noexcept { + m_name = s; +} + + +std::optional project::object(std::string_view name) const noexcept { + + auto rv = std::find_if(m_apps.begin(), m_apps.end(), [name](const auto& o) { return o.name() == name; }); + if( rv != m_apps.end() ) + return *rv; + + rv = std::find_if(m_libs.begin(), m_libs.end(), [name](const auto& o) { return o.name() == name; }); + if( rv != m_libs.end() ) + return *rv; + + rv = std::find_if(m_tests.begin(), m_tests.end(), [name](const auto& o) { return o.name() == name; }); + if( rv != m_tests.end() ) + return *rv; + + return std::optional{}; +} + + +bool project::object_exists(std::string_view name, object::type_t type) const noexcept { + + if( type == object::type_t::any || type == object::type_t::app) { + auto i = std::find_if(m_apps.begin(), m_apps.end(), [name](const auto& o) { return o.name() == name; }); + if( i != m_apps.end() ) + return true; + } + + if( type == object::type_t::any || type == object::type_t::lib) { + auto i = std::find_if(m_libs.begin(), m_libs.end(), [name](const auto& o) { return o.name() == name; }); + if( i != m_libs.end() ) + return true; + } + + if( type == object::type_t::any || type == object::type_t::test) { + auto i = std::find_if(m_tests.begin(), m_tests.end(), [name](const auto& o) { return o.name() == name; }); + if( i != m_tests.end() ) + return true; + } + + return false; +} + + +std::filesystem::path project::path() const noexcept { + return m_path; +} + + +void project::path(const std::filesystem::path& path) noexcept { + m_path = path; +} + + +bool project::remove(std::string_view name, object::type_t type) noexcept { + + bool rv = false; + + if( type == object::any || type == object::app) { + auto i = std::find_if(m_apps.begin(), m_apps.end(), [name](const antler::project::object& o) { return o.name() == name; } ); + if( i != m_apps.end() ) { + m_apps.erase(i); + rv = true; + } + } + + if( type == object::any || type == object::lib) { + auto i = std::find_if(m_libs.begin(), m_libs.end(), [name](const antler::project::object& o) { return o.name() == name; } ); + if( i != m_libs.end() ) { + m_libs.erase(i); + rv = true; + } + } + + if( type == object::any || type == object::test) { + auto i = std::find_if(m_tests.begin(), m_tests.end(), [name](const antler::project::object& o) { return o.name() == name; } ); + if( i != m_tests.end() ) { + m_tests.erase(i); + rv = true; + } + } + + return rv; +} + + +bool project::sync(std::ostream& es) noexcept { + + if(m_path.empty()) { + es << "No path to write to.\n"; + return false; + } + + + try { + + // Open the file. + std::ofstream out(m_path); + if(!out.is_open()) { + es << "Problem opening " << m_path << "\n"; + return false; + } + // Print this project to the file. + print(out); + } + catch(std::exception& e) { + es << "Exception: " << e.what() << "\n"; + return false; + } + + // Now, truly sync. + system("sync"); + return true; +} + + +const object::list_t& project::tests() const noexcept { + return m_tests; +} + + +std::string project::to_yaml() const noexcept { + + // ryml wants to print to a stream, so use a std::stringstream here. + std::stringstream ss; + print(ss); + return ss.str(); +} + + +bool project::update_path(std::filesystem::path& path) noexcept { + + std::error_code sec; + + std::filesystem::path search_path = path; + if(search_path.empty()) + search_path = std::filesystem::current_path(); + else if(search_path.filename().extension() == ".yaml" || search_path.filename().extension() == ".yml") { + // The user passed in an *.yaml file, we just report if it exists as a regular file. + return std::filesystem::is_regular_file(search_path,sec); + } + + for(;;) { + if(std::filesystem::exists(search_path / "project.yaml", sec)) { + path = search_path / "project.yaml"; + return true; + } + if(std::filesystem::exists(search_path / "project.yml", sec)) { + path = search_path / "project.yml"; + return true; + } + if(search_path.empty() || search_path == "/") + break; + search_path = search_path.parent_path(); + } + return false; +} + + +void project::upsert(antler::project::object&& obj) noexcept { + + switch(obj.type()) { + case object::app: upsert_app(std::move(obj)); break; + case object::lib: upsert_lib(std::move(obj)); break; + case object::test: upsert_test(std::move(obj)); break; + + case object::any: + case object::none: + // error state! + std::cerr << "Failed to upsert object with name and type: " << obj.name() << ", " << obj.type() << std::endl; + return; + + // Never add a default. + } +} + + +void project::upsert_app(antler::project::object&& app) noexcept { + + auto i = std::find_if(m_apps.begin(), m_apps.end(), [app](const antler::project::object& o) { return o.name() == app.name(); } ); + if( i != m_apps.end() ) + *i = app; + else + m_apps.emplace_back(app); +} + + +void project::upsert_lib(antler::project::object&& lib) noexcept { + + auto i = std::find_if(m_libs.begin(), m_libs.end(), [lib](const antler::project::object& o) { return o.name() == lib.name(); } ); + if( i != m_libs.end() ) + *i = lib; + else + m_libs.emplace_back(lib); +} + + +void project::upsert_test(antler::project::object&& test) noexcept { + + auto i = std::find_if(m_tests.begin(), m_tests.end(), [test](const antler::project::object& o) { return o.name() == test.name(); } ); + if( i != m_tests.end() ) + *i = test; + else + m_tests.emplace_back(test); +} + + +antler::project::version project::version() const noexcept { + return m_ver; +} + + +void project::version(const antler::project::version& ver) noexcept { + m_ver = ver; +} + + +} // namespace project +} // namespace antler diff --git a/project/src/semver.cpp b/project/src/semver.cpp new file mode 100644 index 0000000..3ba8122 --- /dev/null +++ b/project/src/semver.cpp @@ -0,0 +1,245 @@ +#include +#include +#include + +#include +#include +#include +#include + + + +namespace antler { +namespace project { + + +//--- constructors/destrructor ------------------------------------------------------------------------------------------ + +semver::semver(value_type x, value_type y, value_type z, std::string_view pre_release, std::string_view build) noexcept + : m_xyz{x,y,z} + , m_pre{pre_release} + , m_build{build} +{ +} + + +//--- operators --------------------------------------------------------------------------------------------------------- + +bool semver::operator==(const self& rhs) const noexcept { + return compare(rhs) == cmp_result::eq; +} + + +bool semver::operator!=(const self& rhs) const noexcept { + return compare(rhs) != cmp_result::eq; +} + + +bool semver::operator<(const self& rhs) const noexcept { + return compare(rhs) == cmp_result::lt; +} + + +bool semver::operator<=(const self& rhs) const noexcept { + return compare(rhs) != cmp_result::gt; +} + + +bool semver::operator>(const self& rhs) const noexcept { + return compare(rhs) == cmp_result::gt; +} + + +bool semver::operator>=(const self& rhs) const noexcept { + return compare(rhs) != cmp_result::lt; +} + + +//--- alphabetic -------------------------------------------------------------------------------------------------------- + +void semver::clear() noexcept { + m_xyz.fill(0); + m_pre.clear(); + m_build.clear(); +} + + +cmp_result semver::compare_b_rule12(std::string_view lhs, std::string_view rhs) noexcept { + + // If either is empty, this is a quick comparison. + if(lhs.empty()) + return (rhs.empty() ? cmp_result::eq : cmp_result::lt); + if(rhs.empty()) + return cmp_result::gt; + + return compare_pb_rule12(lhs, rhs); +} + + +cmp_result semver::compare_p_rule12(std::string_view lhs, std::string_view rhs) noexcept { + + // If either is empty, this is a quick comparison. + if(lhs.empty()) + return (rhs.empty() ? cmp_result::eq : cmp_result::gt); + if(rhs.empty()) + return cmp_result::lt; + + return compare_pb_rule12(lhs, rhs); +} + + +cmp_result semver::compare_pb_rule12(std::string_view lhs, std::string_view rhs) noexcept { + + // Requirements here: + // https://semver.org/spec/v2.0.0-rc.1.html#spec-item-12 + + // Split on '.' + auto l = string::split(lhs,"."); + auto r = string::split(rhs,"."); + + // Compare the splits. + const size_t comp_count = std::min(l.size(), r.size()); + for(size_t i = 0; i < comp_count; ++i) { + // Same? On to the next one... + if(l[i] == r[i]) + continue; + // Numbers have higher magnitude than letters, look to see if either value is ALL numbers. + auto left = l[i].find_first_not_of("0123456789"); + auto right = r[i].find_first_not_of("0123456789"); + if(left != std::string_view::npos) { // Left is NOT numbers only. + if(right != std::string_view::npos) { // Also Right is NOT numbers only. + // Simple string compare works here. + if(l[i] < r[i]) + return cmp_result::lt; + return cmp_result::gt; + } + // Left has letters, right is numbers. So right is of higer magnitude. + return cmp_result::lt; + } + // Left is a number, if right is NOT a number, then left has greater magnitude. + if(right != std::string_view::npos) + return cmp_result::gt; + // Both are numbers, convert and compare. + int lnum=0; + string::from(l[i],lnum); // ignore return code. If it's a failure, we will just use the zero value. + int rnum=0; + string::from(r[i],rnum); + if(lnum == rnum) + continue; + if(lnum < rnum) + return cmp_result::lt; + return cmp_result::gt; + } + // So far, all the splits are equal. Is one of them longer then the other? + if(l.size() == r.size()) + return cmp_result::eq; + if(l.size() < r.size()) + return cmp_result::lt; + return cmp_result::gt; +} + + +cmp_result semver::compare(const self& rhs) const noexcept { + // Compare x.y.z + for(size_t i=0; i < m_xyz.size(); ++i) { + if(m_xyz[i] == rhs.m_xyz[i]) + continue; + if(m_xyz[i] < rhs.m_xyz[i]) + return cmp_result::lt; + return cmp_result::gt; + } + // x.y.z are the same, so compare pre-release and build. + auto result = compare_p_rule12(m_pre, rhs.m_pre); + if(result != cmp_result::eq) + return result; + + return compare_b_rule12(m_build, rhs.m_build); +} + + +std::optional semver::parse(std::string_view s) noexcept { + + semver rv; + + // Get build first, if any. + auto pos = s.find_first_of('+'); + if(pos != std::string_view::npos) { + // Copy the build substring, test it's valid, and + rv.m_build = s.substr(pos+1); + if(rv.m_build.empty() || !validate_pb_rule10or11(rv.m_build)) + return std::optional(); + s=s.substr(0,pos); + } + // Now get the pre-release, if any. + // Note we allow the deviation of having rc values NOT require the leading dash ('-'). + pos = s.find_first_of('-'); + if(pos != std::string_view::npos) { // found '-' + // Copy the build substring, test it's valid, and + rv.m_pre = s.substr(pos+1); + if(rv.m_pre.empty() || !validate_pb_rule10or11(rv.m_pre)) + return std::optional(); + s=s.substr(0,pos); + } + else { + pos = s.find("rc"); + if(pos != std::string_view::npos) { // found "rc" + rv.m_pre = s.substr(pos); + if(rv.m_pre.empty() || !validate_pb_rule10or11(rv.m_pre)) + return std::optional(); + s=s.substr(0,pos); + } + } + + + // Split x.y.z apart, validate it as well. + auto splits = string::split(s,"."); + if(splits.empty() || (splits.size() > 3)) + return std::optional(); + for(size_t i=0; i < splits.size(); ++i) { + // From returns false if any value isn't in [0-9]. + if(!string::from(splits[i],rv.m_xyz[i])) + return std::optional(); + } + + return rv; +} + + +void semver::print(std::ostream& os) const noexcept { + + // x.y.z + os << m_xyz[0] << '.' << m_xyz[1] << '.' << m_xyz[2]; + // pre-release? + if(!m_pre.empty()) + os << '-' << m_pre; + // build? + if(!m_build.empty()) + os << '+' << m_build; +} + + +std::string semver::string() const noexcept { + std::ostringstream ss; + print(ss); + return ss.str(); +} + + +bool semver::validate_pb_rule10or11(std::string_view s) noexcept { + for(auto c : s) { + if( (c >= '0' && c <= '9') + || (c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + || (c == '-') ) { + // This char was valid, on to the next one. + continue; + } + return false; + } + // All chars were valid!!! + return true; +} + + +} // namespace project +} // namespace antler diff --git a/project/src/test.cpp b/project/src/test.cpp new file mode 100644 index 0000000..8c60946 --- /dev/null +++ b/project/src/test.cpp @@ -0,0 +1 @@ +#include diff --git a/project/src/version.cpp b/project/src/version.cpp new file mode 100644 index 0000000..0dd47cb --- /dev/null +++ b/project/src/version.cpp @@ -0,0 +1,236 @@ +#include +#include +#include + +#include +#include +#include +#include + + + +namespace antler { +namespace project { + + +//--- constructors/destruct ------------------------------------------------------------------------------------------ + +version::version() = default; + + +version::version(std::string_view ver) +{ + load(ver); +} + + +version::version(const semver& sv) +{ + load(sv); +} + + +version::version(const self& rhs) + : m_raw{rhs.m_raw} +{ + if(rhs.m_semver) + m_semver = std::make_unique(*rhs.m_semver); +} + + +//--- operators --------------------------------------------------------------------------------------------------------- + +version& version::operator=(std::string_view ver) { + load(ver); + return *this; +} + + +version& version::operator=(const semver& sv) { + load(sv); + return *this; +} + + +version& version::operator=(const self& rhs) { + m_raw = rhs.m_raw; + if(rhs.m_semver) + m_semver = std::make_unique(*rhs.m_semver); + else + m_semver.reset(); + return *this; +} + + +bool version::operator==(const self& rhs) const noexcept { + return compare(rhs) == cmp::eq; +} + + +bool version::operator!=(const self& rhs) const noexcept { + return compare(rhs) != cmp::eq; +} + + +bool version::operator<(const self& rhs) const noexcept { + return compare(rhs) == cmp::lt; +} + + +bool version::operator<=(const self& rhs) const noexcept { + return compare(rhs) != cmp::gt; +} + + +bool version::operator>(const self& rhs) const noexcept { + return compare(rhs) == cmp::gt; +} + + +bool version::operator>=(const self& rhs) const noexcept { + return compare(rhs) != cmp::lt; +} + + +version::operator semver() const noexcept { + if(m_semver) + return *m_semver; + return semver{}; +} + + + + +//--- alphabetic -------------------------------------------------------------------------------------------------------- + + +void version::clear() noexcept { + m_raw.clear(); + m_semver.reset(); +} + + +version::cmp version::compare(const self& rhs) const noexcept { + if(is_semver() && rhs.is_semver()) { + if( *m_semver == *rhs.m_semver ) + return cmp::eq; + if( *m_semver < *rhs.m_semver ) + return cmp::lt; + return cmp::gt; + } + return raw_compare(m_raw, rhs.m_raw); +} + + +bool version::empty() const noexcept { + return m_raw.empty(); +} + + +bool version::is_semver() const noexcept { + if( m_semver ) + return true; + return false; +} + + +void version::load(std::string_view s) { + m_raw = s; + auto temp = semver::parse(m_raw); + if(!temp) + m_semver.reset(); + else + m_semver = std::make_unique(temp.value()); +} + + +void version::load(const semver& sv) { + std::stringstream ss; + ss << sv; + m_raw = ss.str(); + m_semver = std::make_unique(sv); +} + + +std::string_view version::raw() const noexcept { + return m_raw; +} + + +version::cmp version::raw_compare(std::string_view l_in, std::string_view r_in) noexcept { + if(l_in == r_in) + return cmp::eq; + + auto l = string::split(l_in,".,-;"); + auto r = string::split(r_in,".,-;"); + + for(size_t i = 0; i < std::min(l.size(), r.size()); ++i) { + if(l[i] == r[i]) + continue; + + int lnum=0; + int rnum=0; + + // Can we convert the whole thing to a number? + auto ln = l[i].find_first_not_of("0123456789"); + auto rn = r[i].find_first_not_of("0123456789"); + if(ln != std::string_view::npos || rn != std::string_view::npos) { + + // Nope, so try to compare numbers and letters. + + std::string_view lremain; + if(ln == std::string_view::npos) { + string::from(l[i],lnum); + lremain = l[i]; + } + else { + string::from(l[i].substr(0,ln),lnum); + lremain = l[i].substr(ln); + } + + std::string_view rremain; + if(rn == std::string_view::npos) { + string::from(r[i],rnum); + rremain = r[i]; + } + else { + string::from(r[i].substr(0,rn),rnum); + rremain = r[i].substr(rn); + } + + if(lnum != rnum) { + if(lnum < rnum) + return cmp::lt; + return cmp::gt; + } + + auto temp = lremain.compare(rremain); + if(temp < 0) + return cmp::lt; + return cmp::gt; + } + + if( !string::from(l[i],lnum) || !string::from(r[i],rnum) ) { + // Nope, STILL can't convert to JUST a number. Just do a raw string compare. + auto temp = l[i].compare(r[i]); + if(temp < 0) + return cmp::lt; + return cmp::gt; + } + if(lnum < rnum) + return cmp::lt; + return cmp::gt; + } + + if(l.size() < r.size()) + return cmp::lt; + if(l.size() > r.size()) + return cmp::gt; + return cmp::eq; +} + + + + +} // namespace project +} // namespace antler diff --git a/project/src/version_compare.cpp b/project/src/version_compare.cpp new file mode 100644 index 0000000..db5152a --- /dev/null +++ b/project/src/version_compare.cpp @@ -0,0 +1,96 @@ +#include +#include +#include + + +namespace antler { +namespace project { + +void print(std::ostream& os, cmp_result e) noexcept { + switch(e) { +#define CASE_OF(X) case cmp_result::X: os << #X; return; + CASE_OF(eq); + CASE_OF(lt); + CASE_OF(gt); +#undef CASE_OF + } + os << "unknown cmp_result (" << unsigned(e) << ")"; +} + + +cmp_result raw_compare(std::string_view lhs, std::string_view rhs) noexcept { + + if(lhs == rhs) + return cmp_result::eq; + + auto l = string::split(lhs,".,-;+"); + auto r = string::split(rhs,".,-;+"); + + for(size_t i = 0; i < std::min(l.size(), r.size()); ++i) { + if(l[i] == r[i]) + continue; + + int lnum=0; + int rnum=0; + + // Can we convert the whole thing to a number? + auto ln = l[i].find_first_not_of("0123456789"); + auto rn = r[i].find_first_not_of("0123456789"); + if(ln != std::string_view::npos || rn != std::string_view::npos) { + + // Nope, so try to compare numbers and letters. + + std::string_view lremain; + if(ln == std::string_view::npos) { + string::from(l[i],lnum); + lremain = l[i]; + } + else { + string::from(l[i].substr(0,ln),lnum); + lremain = l[i].substr(ln); + } + + std::string_view rremain; + if(rn == std::string_view::npos) { + string::from(r[i],rnum); + rremain = r[i]; + } + else { + string::from(r[i].substr(0,rn),rnum); + rremain = r[i].substr(rn); + } + + if(lnum != rnum) { + if(lnum < rnum) + return cmp_result::lt; + return cmp_result::gt; + } + + auto temp = lremain.compare(rremain); + if(temp < 0) + return cmp_result::lt; + return cmp_result::gt; + } + + if( !string::from(l[i],lnum) || !string::from(r[i],rnum) ) { + // Nope, STILL can't convert to JUST a number. Just do a raw string compare. + auto temp = l[i].compare(r[i]); + if(temp < 0) + return cmp_result::lt; + return cmp_result::gt; + } + if(lnum < rnum) + return cmp_result::lt; + return cmp_result::gt; + } + + if(l.size() < r.size()) + return cmp_result::lt; + if(l.size() > r.size()) + return cmp_result::gt; + return cmp_result::eq; +} + + +} // namespace project +} // namespace antler diff --git a/project/src/version_constraint.cpp b/project/src/version_constraint.cpp new file mode 100644 index 0000000..e0eb8ac --- /dev/null +++ b/project/src/version_constraint.cpp @@ -0,0 +1,263 @@ +#include +#include +#include +#include +#include + +#include +#include +#include + + +std::ostream& operator<<(std::ostream& os, std::vector v) { + os << "["; + if(!v.empty()) { + auto i = v.begin(); + os << '"' << *i << '"'; + for(++i; i != v.end(); ++i) + os << ",\"" << *i << '"'; + } + os << "]"; + return os; +} + + +namespace antler { +namespace project { + + +namespace { // anonymous + +constexpr semver::value_type min_semver_val = std::numeric_limits::min(); +constexpr semver::value_type max_semver_val = std::numeric_limits::max(); + +const semver min_semver{ min_semver_val, min_semver_val, min_semver_val }; +const semver max_semver{ max_semver_val, max_semver_val, max_semver_val }; + +const version min_version{ min_semver }; +const version max_version{ max_semver }; + +} // anonymous namespace + + + + +//--- constructors/destructor ------------------------------------------------------------------------------------------ + +version_constraint::version_constraint() = default; + + +version_constraint::version_constraint(std::string_view ver) +{ + load(ver); +} + + +//--- operators --------------------------------------------------------------------------------------------------------- + +version_constraint& version_constraint::operator=(std::string_view ver) { + load(ver); + return *this; +} + + +//--- alphabetic -------------------------------------------------------------------------------------------------------- + +void version_constraint::clear() { + m_raw.clear(); + m_constraints.clear(); +} + + +bool version_constraint::empty() const noexcept { + return m_constraints.empty(); +} + + +bool version_constraint::is_unique() const noexcept { + return m_constraints.size() == 1 && m_constraints[0].inclusivity == bounds_inclusivity::unique; +} + + +void version_constraint::load(std::string_view sin, std::ostream& os) { + + // Trim whitespace from both ends. + auto s = string::trim(sin); + + // Set the raw value and clear constraints in preparation for further parsing. + m_raw = s; + m_constraints.clear(); + + // But return if s is now empty. + if(s.empty()) + return; + + // Start by splitting on '|' + for(auto split : string::split(s,"|;")) { + // Now split on ',' + auto element = string::split(split,","); + if(element.size() == 1) { + // If there's only one constraint, we need to decide if it's an upper bound, a lower bound, or unique. + auto trimmed_el = string::trim(element[0]); + auto el_list = string::split(trimmed_el," "); + + if(el_list.size() == 1) { + // One member MUST be a unique. + auto ver_str = string::trim(el_list[0]); + m_constraints.emplace_back( constraint{version(ver_str), version(), bounds_inclusivity::unique} ); + continue; + } + + if(el_list.size() == 2) { + // Two members is a bound. + auto op_str = string::trim(el_list[0]); + auto ver_str = string::trim(el_list[1]); + if(op_str == "<") + m_constraints.emplace_back( constraint{min_version, version(ver_str), bounds_inclusivity::lower} ); // inclusive of the min! + else if(op_str == "<=") + m_constraints.emplace_back( constraint{min_version, version(ver_str), bounds_inclusivity::both} ); + else if(op_str == ">") + m_constraints.emplace_back( constraint{version(ver_str), max_version, bounds_inclusivity::upper} ); // inclusive of the max! + else if(op_str == ">=") + m_constraints.emplace_back( constraint{version(ver_str), max_version, bounds_inclusivity::both} ); + else + { + os << __FILE__ << ":" << __LINE__ << " Failed to decode version_constraint: \"" << sin << "\" Bad op: \"" << el_list << "\"\n"; + clear(); + return; + } + + continue; + } + + os << __FILE__ << ":" << __LINE__ + << " Failed to decode version_constraint: \"" << sin << "\" Too many or too few elements in: \"" << el_list << "\"\n"; + clear(); + return; + } + + if(element.size() == 2) { + auto lower_list = string::split( string::trim(element[0]), " "); + auto upper_list = string::split( string::trim(element[1]), " "); + + if(lower_list.size() != 2 || upper_list.size() != 2) { + os << __FILE__ << ":" << __LINE__ + << " Failed to decode version_constraint: \"" << sin << "\" Too many or too few elements in: \"" << element << "\"\n"; + clear(); + return; + } + + auto lop = string::trim(lower_list[0]); + auto lver = string::trim(lower_list[1]); + auto uop = string::trim(upper_list[0]); + auto uver = string::trim(upper_list[1]); + if(lop == ">") { + if(uop == "<") { + m_constraints.emplace_back( constraint{version(lver), version(uver), bounds_inclusivity::none} ); + continue; + } + if(uop == "<=") { + m_constraints.emplace_back( constraint{version(lver), version(uver), bounds_inclusivity::upper} ); + continue; + } + + os << __FILE__ << ":" << __LINE__ + << " Failed to decode version_constraint: \"" << sin << "\" Bad upper limit operator in: \"" << element << "\"\n"; + clear(); + return; + } + + if(lop == ">=") { + if(uop == "<") { + m_constraints.emplace_back( constraint{version(lver), version(uver), bounds_inclusivity::lower} ); + continue; + } + if(uop == "<=") { + m_constraints.emplace_back( constraint{version(lver), version(uver), bounds_inclusivity::both} ); + continue; + } + + os << __FILE__ << ":" << __LINE__ + << " Failed to decode version_constraint: \"" << sin << "\" Bad upper limit operator in: \"" << element << "\"\n"; + clear(); + return; + } + + os << __FILE__ << ":" << __LINE__ + << " Failed to decode version_constraint: \"" << sin << "\" Bad lower limit operator in: \"" << element << "\"\n"; + clear(); + return; + } + + os << __FILE__ << ":" << __LINE__ + << " Failed to decode version_constraint: \"" << sin << "\" Too many elements in: \"" << element << "\"\n"; + clear(); + return; + } + + +} + + +void version_constraint::print(std::ostream& os) const noexcept { + + if(m_constraints.empty()) { + os << "unconstrained"; + return; + } + + for(size_t i=0; i < m_constraints.size(); ++i) { + if(i) + os << " | "; + const auto& a = m_constraints[i]; + switch(a.inclusivity) { + case bounds_inclusivity::none: os << ">" << a.lower_bound << ", < " << a.upper_bound; break; + case bounds_inclusivity::lower: os << ">=" << a.lower_bound << ", <" << a.upper_bound; break; + case bounds_inclusivity::upper: os << ">" << a.lower_bound << ", <=" << a.upper_bound; break; + case bounds_inclusivity::both: os << ">=" << a.lower_bound << ", <=" << a.upper_bound; break; + case bounds_inclusivity::unique: os << a.lower_bound; break; + }; + } +} + + +std::string_view version_constraint::raw() const noexcept { + return m_raw; +} + + +bool version_constraint::test(const version& ver) const noexcept { + if(m_constraints.empty()) + return true; + + for(const auto& a : m_constraints) { + switch(a.inclusivity) { + case bounds_inclusivity::none: + if(a.lower_bound < ver && ver < a.upper_bound) + return true; + break; + case bounds_inclusivity::lower: + if(a.lower_bound <= ver && ver < a.upper_bound) + return true; + break; + case bounds_inclusivity::upper: + if(a.lower_bound < ver && ver <= a.upper_bound) + return true; + break; + case bounds_inclusivity::both: + if(a.lower_bound <= ver && ver <= a.upper_bound) + return true; + break; + case bounds_inclusivity::unique: + if(a.lower_bound == ver) + return true; + break; + } + } + return false; +} + + + +} // namespace project +} // namespace antler diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..2ec6cfb --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,16 @@ +include(CTest) + +# I know. But it's appropriate for now. +file(GLOB src_cpp ./*.cpp) + +foreach(file_name IN ITEMS ${src_cpp}) + cmake_path(GET file_name FILENAME temp) + string(REPLACE ".cpp" "" name ${temp}) + set(name "test-${name}") + + add_executable(${name} ${file_name}) + set_property(TARGET ${target_name} PROPERTY CXX_STANDARD 20) + target_link_libraries(${name} antler-project) + target_include_directories(${name} PRIVATE ./) + add_test(NAME ${name} COMMAND ${name}) +endforeach() diff --git a/test/example.yaml b/test/example.yaml new file mode 100644 index 0000000..134f1d2 --- /dev/null +++ b/test/example.yaml @@ -0,0 +1,28 @@ +title: YAML Example + +owner: + name: Tom Preston-Werner + dob: 1979-05-27T07:32:00-08:00 + +database: + server: 192.168.1.1 + ports: [ 8001, 8001, 8002 ] + connection_max: 5000 + enabled: true + +servers: + + alpha: + ip: 10.0.0.1 + dc: eqdc10 + + beta: + ip: 10.0.0.2 + dc: eqdc10 + +clients: + data: [ [gamma, delta], [1, 2] ] + + hosts: + - alpha + - omega diff --git a/test/test_common.h b/test/test_common.h new file mode 100644 index 0000000..8dccd3d --- /dev/null +++ b/test/test_common.h @@ -0,0 +1,30 @@ +#ifndef antelope_test_common_h +#define antelope_test_common_h + + +namespace global { + +int result = 0; + +} // namespace global + + +/// Evaluate X. On failure print STR, X, and increment global result. +/// @param STR Value to print if the tests fails. +#define TEST(STR, X) { \ + if( !(X) ) { \ + std::cerr << __FILE__ << ":" << __LINE__ << " - " << STR << " - error: failed test \"" << #X << "\"\n"; \ + ++global::result; \ + } \ + } \ + /* end TEST */ + + +inline int result() { + + return -global::result; + +} + + +#endif diff --git a/test/version.cpp b/test/version.cpp new file mode 100644 index 0000000..1704113 --- /dev/null +++ b/test/version.cpp @@ -0,0 +1,112 @@ +#include +#include + +#include +#include +#include +#include + +enum cmp_result { + eq, + lt, + gt, +}; + + +struct compare_entry { + std::string l; // left comparison + std::string r; // right comparison + cmp_result expect; // expectation gt, eq, lt. Test can then check gt, gte, lt, lte, neq +}; + +const std::vector compare_list= { + {"1.0.0", "1.0.0", eq}, + {"1.0.0", "1.0", eq}, + {"1.0.0", "1", eq}, + {"1.0", "1.0", eq}, + {"1.0", "1", eq}, + {"1", "1", eq}, + {"2.0.0", "1.0.0", gt}, + {"1.1.2a", "1.1.2b", lt}, + {"1.1.3a", "1.1.2b", gt}, + {"1.2.cat", "1.2.dog", lt}, + {"1.0 alpha", "1.0 bravo", lt}, + {"1.1 alpha", "1.0 bravo", gt}, + {"1.1 charly", "1.1 bravo", gt}, + {"2.1.3", "1.2.3", gt}, + {"2.2.3", "2.2.3a", lt}, + {"2.2.33", "2.2.3a", gt}, + {"2.2.33", "2.2.3a", gt}, + {"1.0.0", "1.0.0-rc1", gt}, + {"1.0.0+build.23", "1.0.0+build.24", lt}, + {"1.0.0+alpha", "1.0.0", gt}, + {"1.0.0-rc1+alpha", "1.0.0-rc2", lt}, + {"1.0.0-rc1+alpha", "1.0.0-rc2+bravo", lt}, + {"1.0.0+3.7.1", "1.0.0+3.6.1", gt}, + {"1.0.0+3.7.1", "1.0.0+3.99.1", lt}, +}; + + +void test_version() { + + for(auto& a : compare_list) { + antler::project::version l(a.l); + antler::project::version r(a.r); + std::stringstream ss; + ss << l << " , " << r; + auto msg = ss.str(); + + switch(a.expect) { + case eq: { + TEST( msg, l == r ); + TEST( msg, l <= r ); + TEST( msg, l >= r ); + TEST( msg, !(l != r) ); + TEST( msg, !(l < r) ); + TEST( msg, !(l > r) ); + TEST( msg, r == l ); + TEST( msg, r <= l ); + TEST( msg, r >= l ); + TEST( msg, !(r != l) ); + TEST( msg, !(r < l) ); + TEST( msg, !(r > l) ); + } break; + case lt: { + TEST( msg, !(l == r) ); + TEST( msg, l <= r ); + TEST( msg, !(l >= r) ); + TEST( msg, l != r ); + TEST( msg, l < r ); + TEST( msg, !(l > r) ); + TEST( msg, !(r == l) ); + TEST( msg, !(r <= l) ); + TEST( msg, r >= l ); + TEST( msg, r != l ); + TEST( msg, !(r < l) ); + TEST( msg, r > l ); + } break; + case gt: { + TEST( msg, !(l == r) ); + TEST( msg, !(l <= r) ); + TEST( msg, l >= r ); + TEST( msg, l != r ); + TEST( msg, !(l < r) ); + TEST( msg, l > r ); + TEST( msg, !(r == l) ); + TEST( msg, r <= l ); + TEST( msg, !(r >= l) ); + TEST( msg, r != l ); + TEST( msg, r < l ); + TEST( msg, !(r > l) ); + } break; + } + } +} + + +int main(int,char**) { + + test_version(); + + return result(); +} diff --git a/test/version_constraint.cpp b/test/version_constraint.cpp new file mode 100644 index 0000000..ca2cc94 --- /dev/null +++ b/test/version_constraint.cpp @@ -0,0 +1,82 @@ +#include +#include + +#include +#include +#include +#include + + +struct constraint_entry { + std::string ver; // left comparison + std::string constraint; // right comparison + bool result; // expected result of testing if constraint contains ver. +}; + +const std::vector compare_list= { + {"999", "", true}, + {"1.0.0", "1.0.0", true}, + {"1.0.0", "<= 1.0.0", true}, + {"1.0.0", ">= 1.0.0", true}, + {"1.0.0", "< 1.0.0", false}, + {"1.0.0", "> 1.0.0", false}, + {"2.1", "> 2, < 3", true}, + + {"2.0.12", ">= 2.0.12, < 2.1 | >= 2.1.3, < 2.2 | >= 2.2.3, < 2.3 | >= 2.3.2, < 3 | >= 3.2", true}, + {"2.1.3", ">= 2.0.12, < 2.1 | >= 2.1.3, < 2.2 | >= 2.2.3, < 2.3 | >= 2.3.2, < 3 | >= 3.2", true}, + {"2.2.3", ">= 2.0.12, < 2.1 | >= 2.1.3, < 2.2 | >= 2.2.3, < 2.3 | >= 2.3.2, < 3 | >= 3.2", true}, + {"2.2.99", ">= 2.0.12, < 2.1 | >= 2.1.3, < 2.2 | >= 2.2.3, < 2.3 | >= 2.3.2, < 3 | >= 3.2", true}, + {"2.3.2", ">= 2.0.12, < 2.1 | >= 2.1.3, < 2.2 | >= 2.2.3, < 2.3 | >= 2.3.2, < 3 | >= 3.2", true}, + {"3.2", ">= 2.0.12, < 2.1 | >= 2.1.3, < 2.2 | >= 2.2.3, < 2.3 | >= 2.3.2, < 3 | >= 3.2", true}, + {"3.3", ">= 2.0.12, < 2.1 | >= 2.1.3, < 2.2 | >= 2.2.3, < 2.3 | >= 2.3.2, < 3 | >= 3.2", true}, + + {"2.0", ">= 2.0.12, < 2.1 | >= 2.1.3, < 2.2 | >= 2.2.3, < 2.3 | >= 2.3.2, < 3 | >= 3.2", false}, + {"2.1", ">= 2.0.12, < 2.1 | >= 2.1.3, < 2.2 | >= 2.2.3, < 2.3 | >= 2.3.2, < 3 | >= 3.2", false}, + {"2.2", ">= 2.0.12, < 2.1 | >= 2.1.3, < 2.2 | >= 2.2.3, < 2.3 | >= 2.3.2, < 3 | >= 3.2", false}, + {"2.3", ">= 2.0.12, < 2.1 | >= 2.1.3, < 2.2 | >= 2.2.3, < 2.3 | >= 2.3.2, < 3 | >= 3.2", false}, + {"3.0", ">= 2.0.12, < 2.1 | >= 2.1.3, < 2.2 | >= 2.2.3, < 2.3 | >= 2.3.2, < 3 | >= 3.2", false}, + {"3.2-rc0", ">= 2.0.12, < 2.1 | >= 2.1.3, < 2.2 | >= 2.2.3, < 2.3 | >= 2.3.2, < 3 | >= 3.2", false}, + +}; + + +void test_version() { + + for(auto& a : compare_list) { + antler::project::version ver(a.ver); + antler::project::version_constraint cons(a.constraint); + std::stringstream ss; + ss << a.ver << " " << a.constraint << " expect " << (a.result ? "true" : "false"); + auto msg = ss.str(); + + TEST(msg, cons.test(ver) == a.result) + } +} + + + + + +/* +struct constraint_entry { + version v; + contstraint c; + bool e; // expectation: constraint wor + //expectation e; +}; + + + +std::vector versions { + { "1.0rc1", ">=1.0.0", false + + +*/ + + +int main(int,char**) { + + test_version(); + + return result(); +} diff --git a/test/version_semver.cpp b/test/version_semver.cpp new file mode 100644 index 0000000..cd8f56a --- /dev/null +++ b/test/version_semver.cpp @@ -0,0 +1,80 @@ +#include +#include + +#include +#include +#include +#include +#include + + + +struct semver_t { + antler::project::semver::value_type x; + antler::project::semver::value_type y; + antler::project::semver::value_type z; + std::string pre; + std::string build; +}; +std::ostream& operator<<(std::ostream& os, const semver_t& sv) { + os << sv.x << "." << sv.y << "." << sv.z; + if(!sv.pre.empty()) + os << "-" << sv.pre; + if(!sv.build.empty()) + os << "+" << sv.build; + return os; +} + + +struct semver_entry { + std::string l; // left comparison + semver_t r; // right comparison +}; + +const std::vector semver_list= { + {"1.0.0", {1,0,0,"",""} }, + {"1.0", {1,0,0,"",""} }, + {"1", {1,0,0,"",""} }, + {"1.0-rc1", {1,0,0,"rc1",""} }, + {"1.0rc1", {1,0,0,"rc1",""} }, + {"1rc1", {1,0,0,"rc1",""} }, + {"1.99.0-rc1", {1,99,0,"rc1",""} }, + {"1.1.1-rc", {1,1,1,"rc",""} }, + +}; + + +void test_semver() { + + for(auto& a : semver_list) { + + + auto temp = antler::project::semver::parse(a.l); + + std::stringstream ss; + ss << a.l << " , " << a.r << " semver::parse(l): "; + if(temp) + ss << temp.value(); + else + ss << "empty"; + auto msg = ss.str(); + + TEST(msg, temp); + if(!temp) + continue; + + auto l = temp.value(); + const auto& r = antler::project::semver(a.r.x, a.r.y, a.r.z, a.r.pre, a.r.build);; + + TEST(msg, l == r); + } + +} + + +int main(int,char**) { + + test_semver(); + + return result(); +}