diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 1c2f8215..329114a8 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -30,11 +30,11 @@ Thank you! 2. Second step 3. Third step -**`dds` Output** +**`bpt` Output** "; - applyFormatting(fmt); - return *this; - } - - void XmlWriter::writeStylesheetRef( std::string const& url ) { - m_os << "\n"; - } - - XmlWriter& XmlWriter::writeBlankLine() { - ensureTagClosed(); - m_os << '\n'; - return *this; - } - - void XmlWriter::ensureTagClosed() { - if( m_tagIsOpen ) { - m_os << '>' << std::flush; - newlineIfNecessary(); - m_tagIsOpen = false; - } - } - - void XmlWriter::applyFormatting(XmlFormatting fmt) { - m_needsNewline = shouldNewline(fmt); - } - - void XmlWriter::writeDeclaration() { - m_os << "\n"; - } - - void XmlWriter::newlineIfNecessary() { - if( m_needsNewline ) { - m_os << std::endl; - m_needsNewline = false; - } - } -} -// end catch_xmlwriter.cpp -// start catch_reporter_bases.cpp - -#include -#include -#include -#include -#include - -namespace Catch { - void prepareExpandedExpression(AssertionResult& result) { - result.getExpandedExpression(); - } - - // Because formatting using c++ streams is stateful, drop down to C is required - // Alternatively we could use stringstream, but its performance is... not good. - std::string getFormattedDuration( double duration ) { - // Max exponent + 1 is required to represent the whole part - // + 1 for decimal point - // + 3 for the 3 decimal places - // + 1 for null terminator - const std::size_t maxDoubleSize = DBL_MAX_10_EXP + 1 + 1 + 3 + 1; - char buffer[maxDoubleSize]; - - // Save previous errno, to prevent sprintf from overwriting it - ErrnoGuard guard; -#ifdef _MSC_VER - sprintf_s(buffer, "%.3f", duration); -#else - std::sprintf(buffer, "%.3f", duration); -#endif - return std::string(buffer); - } - - bool shouldShowDuration( IConfig const& config, double duration ) { - if ( config.showDurations() == ShowDurations::Always ) { - return true; - } - if ( config.showDurations() == ShowDurations::Never ) { - return false; - } - const double min = config.minDuration(); - return min >= 0 && duration >= min; - } - - std::string serializeFilters( std::vector const& container ) { - ReusableStringStream oss; - bool first = true; - for (auto&& filter : container) - { - if (!first) - oss << ' '; - else - first = false; - - oss << filter; - } - return oss.str(); - } - - TestEventListenerBase::TestEventListenerBase(ReporterConfig const & _config) - :StreamingReporterBase(_config) {} - - std::set TestEventListenerBase::getSupportedVerbosities() { - return { Verbosity::Quiet, Verbosity::Normal, Verbosity::High }; - } - - void TestEventListenerBase::assertionStarting(AssertionInfo const &) {} - - bool TestEventListenerBase::assertionEnded(AssertionStats const &) { - return false; - } - -} // end namespace Catch -// end catch_reporter_bases.cpp -// start catch_reporter_compact.cpp - -namespace { - -#ifdef CATCH_PLATFORM_MAC - const char* failedString() { return "FAILED"; } - const char* passedString() { return "PASSED"; } -#else - const char* failedString() { return "failed"; } - const char* passedString() { return "passed"; } -#endif - - // Colour::LightGrey - Catch::Colour::Code dimColour() { return Catch::Colour::FileName; } - - std::string bothOrAll( std::size_t count ) { - return count == 1 ? std::string() : - count == 2 ? "both " : "all " ; - } - -} // anon namespace - -namespace Catch { -namespace { -// Colour, message variants: -// - white: No tests ran. -// - red: Failed [both/all] N test cases, failed [both/all] M assertions. -// - white: Passed [both/all] N test cases (no assertions). -// - red: Failed N tests cases, failed M assertions. -// - green: Passed [both/all] N tests cases with M assertions. -void printTotals(std::ostream& out, const Totals& totals) { - if (totals.testCases.total() == 0) { - out << "No tests ran."; - } else if (totals.testCases.failed == totals.testCases.total()) { - Colour colour(Colour::ResultError); - const std::string qualify_assertions_failed = - totals.assertions.failed == totals.assertions.total() ? - bothOrAll(totals.assertions.failed) : std::string(); - out << - "Failed " << bothOrAll(totals.testCases.failed) - << pluralise(totals.testCases.failed, "test case") << ", " - "failed " << qualify_assertions_failed << - pluralise(totals.assertions.failed, "assertion") << '.'; - } else if (totals.assertions.total() == 0) { - out << - "Passed " << bothOrAll(totals.testCases.total()) - << pluralise(totals.testCases.total(), "test case") - << " (no assertions)."; - } else if (totals.assertions.failed) { - Colour colour(Colour::ResultError); - out << - "Failed " << pluralise(totals.testCases.failed, "test case") << ", " - "failed " << pluralise(totals.assertions.failed, "assertion") << '.'; - } else { - Colour colour(Colour::ResultSuccess); - out << - "Passed " << bothOrAll(totals.testCases.passed) - << pluralise(totals.testCases.passed, "test case") << - " with " << pluralise(totals.assertions.passed, "assertion") << '.'; - } -} - -// Implementation of CompactReporter formatting -class AssertionPrinter { -public: - AssertionPrinter& operator= (AssertionPrinter const&) = delete; - AssertionPrinter(AssertionPrinter const&) = delete; - AssertionPrinter(std::ostream& _stream, AssertionStats const& _stats, bool _printInfoMessages) - : stream(_stream) - , result(_stats.assertionResult) - , messages(_stats.infoMessages) - , itMessage(_stats.infoMessages.begin()) - , printInfoMessages(_printInfoMessages) {} - - void print() { - printSourceInfo(); - - itMessage = messages.begin(); - - switch (result.getResultType()) { - case ResultWas::Ok: - printResultType(Colour::ResultSuccess, passedString()); - printOriginalExpression(); - printReconstructedExpression(); - if (!result.hasExpression()) - printRemainingMessages(Colour::None); - else - printRemainingMessages(); - break; - case ResultWas::ExpressionFailed: - if (result.isOk()) - printResultType(Colour::ResultSuccess, failedString() + std::string(" - but was ok")); - else - printResultType(Colour::Error, failedString()); - printOriginalExpression(); - printReconstructedExpression(); - printRemainingMessages(); - break; - case ResultWas::ThrewException: - printResultType(Colour::Error, failedString()); - printIssue("unexpected exception with message:"); - printMessage(); - printExpressionWas(); - printRemainingMessages(); - break; - case ResultWas::FatalErrorCondition: - printResultType(Colour::Error, failedString()); - printIssue("fatal error condition with message:"); - printMessage(); - printExpressionWas(); - printRemainingMessages(); - break; - case ResultWas::DidntThrowException: - printResultType(Colour::Error, failedString()); - printIssue("expected exception, got none"); - printExpressionWas(); - printRemainingMessages(); - break; - case ResultWas::Info: - printResultType(Colour::None, "info"); - printMessage(); - printRemainingMessages(); - break; - case ResultWas::Warning: - printResultType(Colour::None, "warning"); - printMessage(); - printRemainingMessages(); - break; - case ResultWas::ExplicitFailure: - printResultType(Colour::Error, failedString()); - printIssue("explicitly"); - printRemainingMessages(Colour::None); - break; - // These cases are here to prevent compiler warnings - case ResultWas::Unknown: - case ResultWas::FailureBit: - case ResultWas::Exception: - printResultType(Colour::Error, "** internal error **"); - break; - } - } - -private: - void printSourceInfo() const { - Colour colourGuard(Colour::FileName); - stream << result.getSourceInfo() << ':'; - } - - void printResultType(Colour::Code colour, std::string const& passOrFail) const { - if (!passOrFail.empty()) { - { - Colour colourGuard(colour); - stream << ' ' << passOrFail; - } - stream << ':'; - } - } - - void printIssue(std::string const& issue) const { - stream << ' ' << issue; - } - - void printExpressionWas() { - if (result.hasExpression()) { - stream << ';'; - { - Colour colour(dimColour()); - stream << " expression was:"; - } - printOriginalExpression(); - } - } - - void printOriginalExpression() const { - if (result.hasExpression()) { - stream << ' ' << result.getExpression(); - } - } - - void printReconstructedExpression() const { - if (result.hasExpandedExpression()) { - { - Colour colour(dimColour()); - stream << " for: "; - } - stream << result.getExpandedExpression(); - } - } - - void printMessage() { - if (itMessage != messages.end()) { - stream << " '" << itMessage->message << '\''; - ++itMessage; - } - } - - void printRemainingMessages(Colour::Code colour = dimColour()) { - if (itMessage == messages.end()) - return; - - const auto itEnd = messages.cend(); - const auto N = static_cast(std::distance(itMessage, itEnd)); - - { - Colour colourGuard(colour); - stream << " with " << pluralise(N, "message") << ':'; - } - - while (itMessage != itEnd) { - // If this assertion is a warning ignore any INFO messages - if (printInfoMessages || itMessage->type != ResultWas::Info) { - printMessage(); - if (itMessage != itEnd) { - Colour colourGuard(dimColour()); - stream << " and"; - } - continue; - } - ++itMessage; - } - } - -private: - std::ostream& stream; - AssertionResult const& result; - std::vector messages; - std::vector::const_iterator itMessage; - bool printInfoMessages; -}; - -} // anon namespace - - std::string CompactReporter::getDescription() { - return "Reports test results on a single line, suitable for IDEs"; - } - - void CompactReporter::noMatchingTestCases( std::string const& spec ) { - stream << "No test cases matched '" << spec << '\'' << std::endl; - } - - void CompactReporter::assertionStarting( AssertionInfo const& ) {} - - bool CompactReporter::assertionEnded( AssertionStats const& _assertionStats ) { - AssertionResult const& result = _assertionStats.assertionResult; - - bool printInfoMessages = true; - - // Drop out if result was successful and we're not printing those - if( !m_config->includeSuccessfulResults() && result.isOk() ) { - if( result.getResultType() != ResultWas::Warning ) - return false; - printInfoMessages = false; - } - - AssertionPrinter printer( stream, _assertionStats, printInfoMessages ); - printer.print(); - - stream << std::endl; - return true; - } - - void CompactReporter::sectionEnded(SectionStats const& _sectionStats) { - double dur = _sectionStats.durationInSeconds; - if ( shouldShowDuration( *m_config, dur ) ) { - stream << getFormattedDuration( dur ) << " s: " << _sectionStats.sectionInfo.name << std::endl; - } - } - - void CompactReporter::testRunEnded( TestRunStats const& _testRunStats ) { - printTotals( stream, _testRunStats.totals ); - stream << '\n' << std::endl; - StreamingReporterBase::testRunEnded( _testRunStats ); - } - - CompactReporter::~CompactReporter() {} - - CATCH_REGISTER_REPORTER( "compact", CompactReporter ) - -} // end namespace Catch -// end catch_reporter_compact.cpp -// start catch_reporter_console.cpp - -#include -#include - -#if defined(_MSC_VER) -#pragma warning(push) -#pragma warning(disable:4061) // Not all labels are EXPLICITLY handled in switch - // Note that 4062 (not all labels are handled and default is missing) is enabled -#endif - -#if defined(__clang__) -# pragma clang diagnostic push -// For simplicity, benchmarking-only helpers are always enabled -# pragma clang diagnostic ignored "-Wunused-function" -#endif - -namespace Catch { - -namespace { - -// Formatter impl for ConsoleReporter -class ConsoleAssertionPrinter { -public: - ConsoleAssertionPrinter& operator= (ConsoleAssertionPrinter const&) = delete; - ConsoleAssertionPrinter(ConsoleAssertionPrinter const&) = delete; - ConsoleAssertionPrinter(std::ostream& _stream, AssertionStats const& _stats, bool _printInfoMessages) - : stream(_stream), - stats(_stats), - result(_stats.assertionResult), - colour(Colour::None), - message(result.getMessage()), - messages(_stats.infoMessages), - printInfoMessages(_printInfoMessages) { - switch (result.getResultType()) { - case ResultWas::Ok: - colour = Colour::Success; - passOrFail = "PASSED"; - //if( result.hasMessage() ) - if (_stats.infoMessages.size() == 1) - messageLabel = "with message"; - if (_stats.infoMessages.size() > 1) - messageLabel = "with messages"; - break; - case ResultWas::ExpressionFailed: - if (result.isOk()) { - colour = Colour::Success; - passOrFail = "FAILED - but was ok"; - } else { - colour = Colour::Error; - passOrFail = "FAILED"; - } - if (_stats.infoMessages.size() == 1) - messageLabel = "with message"; - if (_stats.infoMessages.size() > 1) - messageLabel = "with messages"; - break; - case ResultWas::ThrewException: - colour = Colour::Error; - passOrFail = "FAILED"; - messageLabel = "due to unexpected exception with "; - if (_stats.infoMessages.size() == 1) - messageLabel += "message"; - if (_stats.infoMessages.size() > 1) - messageLabel += "messages"; - break; - case ResultWas::FatalErrorCondition: - colour = Colour::Error; - passOrFail = "FAILED"; - messageLabel = "due to a fatal error condition"; - break; - case ResultWas::DidntThrowException: - colour = Colour::Error; - passOrFail = "FAILED"; - messageLabel = "because no exception was thrown where one was expected"; - break; - case ResultWas::Info: - messageLabel = "info"; - break; - case ResultWas::Warning: - messageLabel = "warning"; - break; - case ResultWas::ExplicitFailure: - passOrFail = "FAILED"; - colour = Colour::Error; - if (_stats.infoMessages.size() == 1) - messageLabel = "explicitly with message"; - if (_stats.infoMessages.size() > 1) - messageLabel = "explicitly with messages"; - break; - // These cases are here to prevent compiler warnings - case ResultWas::Unknown: - case ResultWas::FailureBit: - case ResultWas::Exception: - passOrFail = "** internal error **"; - colour = Colour::Error; - break; - } - } - - void print() const { - printSourceInfo(); - if (stats.totals.assertions.total() > 0) { - printResultType(); - printOriginalExpression(); - printReconstructedExpression(); - } else { - stream << '\n'; - } - printMessage(); - } - -private: - void printResultType() const { - if (!passOrFail.empty()) { - Colour colourGuard(colour); - stream << passOrFail << ":\n"; - } - } - void printOriginalExpression() const { - if (result.hasExpression()) { - Colour colourGuard(Colour::OriginalExpression); - stream << " "; - stream << result.getExpressionInMacro(); - stream << '\n'; - } - } - void printReconstructedExpression() const { - if (result.hasExpandedExpression()) { - stream << "with expansion:\n"; - Colour colourGuard(Colour::ReconstructedExpression); - stream << Column(result.getExpandedExpression()).indent(2) << '\n'; - } - } - void printMessage() const { - if (!messageLabel.empty()) - stream << messageLabel << ':' << '\n'; - for (auto const& msg : messages) { - // If this assertion is a warning ignore any INFO messages - if (printInfoMessages || msg.type != ResultWas::Info) - stream << Column(msg.message).indent(2) << '\n'; - } - } - void printSourceInfo() const { - Colour colourGuard(Colour::FileName); - stream << result.getSourceInfo() << ": "; - } - - std::ostream& stream; - AssertionStats const& stats; - AssertionResult const& result; - Colour::Code colour; - std::string passOrFail; - std::string messageLabel; - std::string message; - std::vector messages; - bool printInfoMessages; -}; - -std::size_t makeRatio(std::size_t number, std::size_t total) { - std::size_t ratio = total > 0 ? CATCH_CONFIG_CONSOLE_WIDTH * number / total : 0; - return (ratio == 0 && number > 0) ? 1 : ratio; -} - -std::size_t& findMax(std::size_t& i, std::size_t& j, std::size_t& k) { - if (i > j && i > k) - return i; - else if (j > k) - return j; - else - return k; -} - -struct ColumnInfo { - enum Justification { Left, Right }; - std::string name; - int width; - Justification justification; -}; -struct ColumnBreak {}; -struct RowBreak {}; - -class Duration { - enum class Unit { - Auto, - Nanoseconds, - Microseconds, - Milliseconds, - Seconds, - Minutes - }; - static const uint64_t s_nanosecondsInAMicrosecond = 1000; - static const uint64_t s_nanosecondsInAMillisecond = 1000 * s_nanosecondsInAMicrosecond; - static const uint64_t s_nanosecondsInASecond = 1000 * s_nanosecondsInAMillisecond; - static const uint64_t s_nanosecondsInAMinute = 60 * s_nanosecondsInASecond; - - double m_inNanoseconds; - Unit m_units; - -public: - explicit Duration(double inNanoseconds, Unit units = Unit::Auto) - : m_inNanoseconds(inNanoseconds), - m_units(units) { - if (m_units == Unit::Auto) { - if (m_inNanoseconds < s_nanosecondsInAMicrosecond) - m_units = Unit::Nanoseconds; - else if (m_inNanoseconds < s_nanosecondsInAMillisecond) - m_units = Unit::Microseconds; - else if (m_inNanoseconds < s_nanosecondsInASecond) - m_units = Unit::Milliseconds; - else if (m_inNanoseconds < s_nanosecondsInAMinute) - m_units = Unit::Seconds; - else - m_units = Unit::Minutes; - } - - } - - auto value() const -> double { - switch (m_units) { - case Unit::Microseconds: - return m_inNanoseconds / static_cast(s_nanosecondsInAMicrosecond); - case Unit::Milliseconds: - return m_inNanoseconds / static_cast(s_nanosecondsInAMillisecond); - case Unit::Seconds: - return m_inNanoseconds / static_cast(s_nanosecondsInASecond); - case Unit::Minutes: - return m_inNanoseconds / static_cast(s_nanosecondsInAMinute); - default: - return m_inNanoseconds; - } - } - auto unitsAsString() const -> std::string { - switch (m_units) { - case Unit::Nanoseconds: - return "ns"; - case Unit::Microseconds: - return "us"; - case Unit::Milliseconds: - return "ms"; - case Unit::Seconds: - return "s"; - case Unit::Minutes: - return "m"; - default: - return "** internal error **"; - } - - } - friend auto operator << (std::ostream& os, Duration const& duration) -> std::ostream& { - return os << duration.value() << ' ' << duration.unitsAsString(); - } -}; -} // end anon namespace - -class TablePrinter { - std::ostream& m_os; - std::vector m_columnInfos; - std::ostringstream m_oss; - int m_currentColumn = -1; - bool m_isOpen = false; - -public: - TablePrinter( std::ostream& os, std::vector columnInfos ) - : m_os( os ), - m_columnInfos( std::move( columnInfos ) ) {} - - auto columnInfos() const -> std::vector const& { - return m_columnInfos; - } - - void open() { - if (!m_isOpen) { - m_isOpen = true; - *this << RowBreak(); - - Columns headerCols; - Spacer spacer(2); - for (auto const& info : m_columnInfos) { - headerCols += Column(info.name).width(static_cast(info.width - 2)); - headerCols += spacer; - } - m_os << headerCols << '\n'; - - m_os << Catch::getLineOfChars<'-'>() << '\n'; - } - } - void close() { - if (m_isOpen) { - *this << RowBreak(); - m_os << std::endl; - m_isOpen = false; - } - } - - template - friend TablePrinter& operator << (TablePrinter& tp, T const& value) { - tp.m_oss << value; - return tp; - } - - friend TablePrinter& operator << (TablePrinter& tp, ColumnBreak) { - auto colStr = tp.m_oss.str(); - const auto strSize = colStr.size(); - tp.m_oss.str(""); - tp.open(); - if (tp.m_currentColumn == static_cast(tp.m_columnInfos.size() - 1)) { - tp.m_currentColumn = -1; - tp.m_os << '\n'; - } - tp.m_currentColumn++; - - auto colInfo = tp.m_columnInfos[tp.m_currentColumn]; - auto padding = (strSize + 1 < static_cast(colInfo.width)) - ? std::string(colInfo.width - (strSize + 1), ' ') - : std::string(); - if (colInfo.justification == ColumnInfo::Left) - tp.m_os << colStr << padding << ' '; - else - tp.m_os << padding << colStr << ' '; - return tp; - } - - friend TablePrinter& operator << (TablePrinter& tp, RowBreak) { - if (tp.m_currentColumn > 0) { - tp.m_os << '\n'; - tp.m_currentColumn = -1; - } - return tp; - } -}; - -ConsoleReporter::ConsoleReporter(ReporterConfig const& config) - : StreamingReporterBase(config), - m_tablePrinter(new TablePrinter(config.stream(), - [&config]() -> std::vector { - if (config.fullConfig()->benchmarkNoAnalysis()) - { - return{ - { "benchmark name", CATCH_CONFIG_CONSOLE_WIDTH - 43, ColumnInfo::Left }, - { " samples", 14, ColumnInfo::Right }, - { " iterations", 14, ColumnInfo::Right }, - { " mean", 14, ColumnInfo::Right } - }; - } - else - { - return{ - { "benchmark name", CATCH_CONFIG_CONSOLE_WIDTH - 43, ColumnInfo::Left }, - { "samples mean std dev", 14, ColumnInfo::Right }, - { "iterations low mean low std dev", 14, ColumnInfo::Right }, - { "estimated high mean high std dev", 14, ColumnInfo::Right } - }; - } - }())) {} -ConsoleReporter::~ConsoleReporter() = default; - -std::string ConsoleReporter::getDescription() { - return "Reports test results as plain lines of text"; -} - -void ConsoleReporter::noMatchingTestCases(std::string const& spec) { - stream << "No test cases matched '" << spec << '\'' << std::endl; -} - -void ConsoleReporter::reportInvalidArguments(std::string const&arg){ - stream << "Invalid Filter: " << arg << std::endl; -} - -void ConsoleReporter::assertionStarting(AssertionInfo const&) {} - -bool ConsoleReporter::assertionEnded(AssertionStats const& _assertionStats) { - AssertionResult const& result = _assertionStats.assertionResult; - - bool includeResults = m_config->includeSuccessfulResults() || !result.isOk(); - - // Drop out if result was successful but we're not printing them. - if (!includeResults && result.getResultType() != ResultWas::Warning) - return false; - - lazyPrint(); - - ConsoleAssertionPrinter printer(stream, _assertionStats, includeResults); - printer.print(); - stream << std::endl; - return true; -} - -void ConsoleReporter::sectionStarting(SectionInfo const& _sectionInfo) { - m_tablePrinter->close(); - m_headerPrinted = false; - StreamingReporterBase::sectionStarting(_sectionInfo); -} -void ConsoleReporter::sectionEnded(SectionStats const& _sectionStats) { - m_tablePrinter->close(); - if (_sectionStats.missingAssertions) { - lazyPrint(); - Colour colour(Colour::ResultError); - if (m_sectionStack.size() > 1) - stream << "\nNo assertions in section"; - else - stream << "\nNo assertions in test case"; - stream << " '" << _sectionStats.sectionInfo.name << "'\n" << std::endl; - } - double dur = _sectionStats.durationInSeconds; - if (shouldShowDuration(*m_config, dur)) { - stream << getFormattedDuration(dur) << " s: " << _sectionStats.sectionInfo.name << std::endl; - } - if (m_headerPrinted) { - m_headerPrinted = false; - } - StreamingReporterBase::sectionEnded(_sectionStats); -} - -#if defined(CATCH_CONFIG_ENABLE_BENCHMARKING) -void ConsoleReporter::benchmarkPreparing(std::string const& name) { - lazyPrintWithoutClosingBenchmarkTable(); - - auto nameCol = Column(name).width(static_cast(m_tablePrinter->columnInfos()[0].width - 2)); - - bool firstLine = true; - for (auto line : nameCol) { - if (!firstLine) - (*m_tablePrinter) << ColumnBreak() << ColumnBreak() << ColumnBreak(); - else - firstLine = false; - - (*m_tablePrinter) << line << ColumnBreak(); - } -} - -void ConsoleReporter::benchmarkStarting(BenchmarkInfo const& info) { - (*m_tablePrinter) << info.samples << ColumnBreak() - << info.iterations << ColumnBreak(); - if (!m_config->benchmarkNoAnalysis()) - (*m_tablePrinter) << Duration(info.estimatedDuration) << ColumnBreak(); -} -void ConsoleReporter::benchmarkEnded(BenchmarkStats<> const& stats) { - if (m_config->benchmarkNoAnalysis()) - { - (*m_tablePrinter) << Duration(stats.mean.point.count()) << ColumnBreak(); - } - else - { - (*m_tablePrinter) << ColumnBreak() - << Duration(stats.mean.point.count()) << ColumnBreak() - << Duration(stats.mean.lower_bound.count()) << ColumnBreak() - << Duration(stats.mean.upper_bound.count()) << ColumnBreak() << ColumnBreak() - << Duration(stats.standardDeviation.point.count()) << ColumnBreak() - << Duration(stats.standardDeviation.lower_bound.count()) << ColumnBreak() - << Duration(stats.standardDeviation.upper_bound.count()) << ColumnBreak() << ColumnBreak() << ColumnBreak() << ColumnBreak() << ColumnBreak(); - } -} - -void ConsoleReporter::benchmarkFailed(std::string const& error) { - Colour colour(Colour::Red); - (*m_tablePrinter) - << "Benchmark failed (" << error << ')' - << ColumnBreak() << RowBreak(); -} -#endif // CATCH_CONFIG_ENABLE_BENCHMARKING - -void ConsoleReporter::testCaseEnded(TestCaseStats const& _testCaseStats) { - m_tablePrinter->close(); - StreamingReporterBase::testCaseEnded(_testCaseStats); - m_headerPrinted = false; -} -void ConsoleReporter::testGroupEnded(TestGroupStats const& _testGroupStats) { - if (currentGroupInfo.used) { - printSummaryDivider(); - stream << "Summary for group '" << _testGroupStats.groupInfo.name << "':\n"; - printTotals(_testGroupStats.totals); - stream << '\n' << std::endl; - } - StreamingReporterBase::testGroupEnded(_testGroupStats); -} -void ConsoleReporter::testRunEnded(TestRunStats const& _testRunStats) { - printTotalsDivider(_testRunStats.totals); - printTotals(_testRunStats.totals); - stream << std::endl; - StreamingReporterBase::testRunEnded(_testRunStats); -} -void ConsoleReporter::testRunStarting(TestRunInfo const& _testInfo) { - StreamingReporterBase::testRunStarting(_testInfo); - printTestFilters(); -} - -void ConsoleReporter::lazyPrint() { - - m_tablePrinter->close(); - lazyPrintWithoutClosingBenchmarkTable(); -} - -void ConsoleReporter::lazyPrintWithoutClosingBenchmarkTable() { - - if (!currentTestRunInfo.used) - lazyPrintRunInfo(); - if (!currentGroupInfo.used) - lazyPrintGroupInfo(); - - if (!m_headerPrinted) { - printTestCaseAndSectionHeader(); - m_headerPrinted = true; - } -} -void ConsoleReporter::lazyPrintRunInfo() { - stream << '\n' << getLineOfChars<'~'>() << '\n'; - Colour colour(Colour::SecondaryText); - stream << currentTestRunInfo->name - << " is a Catch v" << libraryVersion() << " host application.\n" - << "Run with -? for options\n\n"; - - if (m_config->rngSeed() != 0) - stream << "Randomness seeded to: " << m_config->rngSeed() << "\n\n"; - - currentTestRunInfo.used = true; -} -void ConsoleReporter::lazyPrintGroupInfo() { - if (!currentGroupInfo->name.empty() && currentGroupInfo->groupsCounts > 1) { - printClosedHeader("Group: " + currentGroupInfo->name); - currentGroupInfo.used = true; - } -} -void ConsoleReporter::printTestCaseAndSectionHeader() { - assert(!m_sectionStack.empty()); - printOpenHeader(currentTestCaseInfo->name); - - if (m_sectionStack.size() > 1) { - Colour colourGuard(Colour::Headers); - - auto - it = m_sectionStack.begin() + 1, // Skip first section (test case) - itEnd = m_sectionStack.end(); - for (; it != itEnd; ++it) - printHeaderString(it->name, 2); - } - - SourceLineInfo lineInfo = m_sectionStack.back().lineInfo; - - stream << getLineOfChars<'-'>() << '\n'; - Colour colourGuard(Colour::FileName); - stream << lineInfo << '\n'; - stream << getLineOfChars<'.'>() << '\n' << std::endl; -} - -void ConsoleReporter::printClosedHeader(std::string const& _name) { - printOpenHeader(_name); - stream << getLineOfChars<'.'>() << '\n'; -} -void ConsoleReporter::printOpenHeader(std::string const& _name) { - stream << getLineOfChars<'-'>() << '\n'; - { - Colour colourGuard(Colour::Headers); - printHeaderString(_name); - } -} - -// if string has a : in first line will set indent to follow it on -// subsequent lines -void ConsoleReporter::printHeaderString(std::string const& _string, std::size_t indent) { - std::size_t i = _string.find(": "); - if (i != std::string::npos) - i += 2; - else - i = 0; - stream << Column(_string).indent(indent + i).initialIndent(indent) << '\n'; -} - -struct SummaryColumn { - - SummaryColumn( std::string _label, Colour::Code _colour ) - : label( std::move( _label ) ), - colour( _colour ) {} - SummaryColumn addRow( std::size_t count ) { - ReusableStringStream rss; - rss << count; - std::string row = rss.str(); - for (auto& oldRow : rows) { - while (oldRow.size() < row.size()) - oldRow = ' ' + oldRow; - while (oldRow.size() > row.size()) - row = ' ' + row; - } - rows.push_back(row); - return *this; - } - - std::string label; - Colour::Code colour; - std::vector rows; - -}; - -void ConsoleReporter::printTotals( Totals const& totals ) { - if (totals.testCases.total() == 0) { - stream << Colour(Colour::Warning) << "No tests ran\n"; - } else if (totals.assertions.total() > 0 && totals.testCases.allPassed()) { - stream << Colour(Colour::ResultSuccess) << "All tests passed"; - stream << " (" - << pluralise(totals.assertions.passed, "assertion") << " in " - << pluralise(totals.testCases.passed, "test case") << ')' - << '\n'; - } else { - - std::vector columns; - columns.push_back(SummaryColumn("", Colour::None) - .addRow(totals.testCases.total()) - .addRow(totals.assertions.total())); - columns.push_back(SummaryColumn("passed", Colour::Success) - .addRow(totals.testCases.passed) - .addRow(totals.assertions.passed)); - columns.push_back(SummaryColumn("failed", Colour::ResultError) - .addRow(totals.testCases.failed) - .addRow(totals.assertions.failed)); - columns.push_back(SummaryColumn("failed as expected", Colour::ResultExpectedFailure) - .addRow(totals.testCases.failedButOk) - .addRow(totals.assertions.failedButOk)); - - printSummaryRow("test cases", columns, 0); - printSummaryRow("assertions", columns, 1); - } -} -void ConsoleReporter::printSummaryRow(std::string const& label, std::vector const& cols, std::size_t row) { - for (auto col : cols) { - std::string value = col.rows[row]; - if (col.label.empty()) { - stream << label << ": "; - if (value != "0") - stream << value; - else - stream << Colour(Colour::Warning) << "- none -"; - } else if (value != "0") { - stream << Colour(Colour::LightGrey) << " | "; - stream << Colour(col.colour) - << value << ' ' << col.label; - } - } - stream << '\n'; -} - -void ConsoleReporter::printTotalsDivider(Totals const& totals) { - if (totals.testCases.total() > 0) { - std::size_t failedRatio = makeRatio(totals.testCases.failed, totals.testCases.total()); - std::size_t failedButOkRatio = makeRatio(totals.testCases.failedButOk, totals.testCases.total()); - std::size_t passedRatio = makeRatio(totals.testCases.passed, totals.testCases.total()); - while (failedRatio + failedButOkRatio + passedRatio < CATCH_CONFIG_CONSOLE_WIDTH - 1) - findMax(failedRatio, failedButOkRatio, passedRatio)++; - while (failedRatio + failedButOkRatio + passedRatio > CATCH_CONFIG_CONSOLE_WIDTH - 1) - findMax(failedRatio, failedButOkRatio, passedRatio)--; - - stream << Colour(Colour::Error) << std::string(failedRatio, '='); - stream << Colour(Colour::ResultExpectedFailure) << std::string(failedButOkRatio, '='); - if (totals.testCases.allPassed()) - stream << Colour(Colour::ResultSuccess) << std::string(passedRatio, '='); - else - stream << Colour(Colour::Success) << std::string(passedRatio, '='); - } else { - stream << Colour(Colour::Warning) << std::string(CATCH_CONFIG_CONSOLE_WIDTH - 1, '='); - } - stream << '\n'; -} -void ConsoleReporter::printSummaryDivider() { - stream << getLineOfChars<'-'>() << '\n'; -} - -void ConsoleReporter::printTestFilters() { - if (m_config->testSpec().hasFilters()) { - Colour guard(Colour::BrightYellow); - stream << "Filters: " << serializeFilters(m_config->getTestsOrTags()) << '\n'; - } -} - -CATCH_REGISTER_REPORTER("console", ConsoleReporter) - -} // end namespace Catch - -#if defined(_MSC_VER) -#pragma warning(pop) -#endif - -#if defined(__clang__) -# pragma clang diagnostic pop -#endif -// end catch_reporter_console.cpp -// start catch_reporter_junit.cpp - -#include -#include -#include -#include - -namespace Catch { - - namespace { - std::string getCurrentTimestamp() { - // Beware, this is not reentrant because of backward compatibility issues - // Also, UTC only, again because of backward compatibility (%z is C++11) - time_t rawtime; - std::time(&rawtime); - auto const timeStampSize = sizeof("2017-01-16T17:06:45Z"); - -#ifdef _MSC_VER - std::tm timeInfo = {}; - gmtime_s(&timeInfo, &rawtime); -#else - std::tm* timeInfo; - timeInfo = std::gmtime(&rawtime); -#endif - - char timeStamp[timeStampSize]; - const char * const fmt = "%Y-%m-%dT%H:%M:%SZ"; - -#ifdef _MSC_VER - std::strftime(timeStamp, timeStampSize, fmt, &timeInfo); -#else - std::strftime(timeStamp, timeStampSize, fmt, timeInfo); -#endif - return std::string(timeStamp); - } - - std::string fileNameTag(const std::vector &tags) { - auto it = std::find_if(begin(tags), - end(tags), - [] (std::string const& tag) {return tag.front() == '#'; }); - if (it != tags.end()) - return it->substr(1); - return std::string(); - } - } // anonymous namespace - - JunitReporter::JunitReporter( ReporterConfig const& _config ) - : CumulativeReporterBase( _config ), - xml( _config.stream() ) - { - m_reporterPrefs.shouldRedirectStdOut = true; - m_reporterPrefs.shouldReportAllAssertions = true; - } - - JunitReporter::~JunitReporter() {} - - std::string JunitReporter::getDescription() { - return "Reports test results in an XML format that looks like Ant's junitreport target"; - } - - void JunitReporter::noMatchingTestCases( std::string const& /*spec*/ ) {} - - void JunitReporter::testRunStarting( TestRunInfo const& runInfo ) { - CumulativeReporterBase::testRunStarting( runInfo ); - xml.startElement( "testsuites" ); - } - - void JunitReporter::testGroupStarting( GroupInfo const& groupInfo ) { - suiteTimer.start(); - stdOutForSuite.clear(); - stdErrForSuite.clear(); - unexpectedExceptions = 0; - CumulativeReporterBase::testGroupStarting( groupInfo ); - } - - void JunitReporter::testCaseStarting( TestCaseInfo const& testCaseInfo ) { - m_okToFail = testCaseInfo.okToFail(); - } - - bool JunitReporter::assertionEnded( AssertionStats const& assertionStats ) { - if( assertionStats.assertionResult.getResultType() == ResultWas::ThrewException && !m_okToFail ) - unexpectedExceptions++; - return CumulativeReporterBase::assertionEnded( assertionStats ); - } - - void JunitReporter::testCaseEnded( TestCaseStats const& testCaseStats ) { - stdOutForSuite += testCaseStats.stdOut; - stdErrForSuite += testCaseStats.stdErr; - CumulativeReporterBase::testCaseEnded( testCaseStats ); - } - - void JunitReporter::testGroupEnded( TestGroupStats const& testGroupStats ) { - double suiteTime = suiteTimer.getElapsedSeconds(); - CumulativeReporterBase::testGroupEnded( testGroupStats ); - writeGroup( *m_testGroups.back(), suiteTime ); - } - - void JunitReporter::testRunEndedCumulative() { - xml.endElement(); - } - - void JunitReporter::writeGroup( TestGroupNode const& groupNode, double suiteTime ) { - XmlWriter::ScopedElement e = xml.scopedElement( "testsuite" ); - - TestGroupStats const& stats = groupNode.value; - xml.writeAttribute( "name", stats.groupInfo.name ); - xml.writeAttribute( "errors", unexpectedExceptions ); - xml.writeAttribute( "failures", stats.totals.assertions.failed-unexpectedExceptions ); - xml.writeAttribute( "tests", stats.totals.assertions.total() ); - xml.writeAttribute( "hostname", "tbd" ); // !TBD - if( m_config->showDurations() == ShowDurations::Never ) - xml.writeAttribute( "time", "" ); - else - xml.writeAttribute( "time", suiteTime ); - xml.writeAttribute( "timestamp", getCurrentTimestamp() ); - - // Write properties if there are any - if (m_config->hasTestFilters() || m_config->rngSeed() != 0) { - auto properties = xml.scopedElement("properties"); - if (m_config->hasTestFilters()) { - xml.scopedElement("property") - .writeAttribute("name", "filters") - .writeAttribute("value", serializeFilters(m_config->getTestsOrTags())); - } - if (m_config->rngSeed() != 0) { - xml.scopedElement("property") - .writeAttribute("name", "random-seed") - .writeAttribute("value", m_config->rngSeed()); - } - } - - // Write test cases - for( auto const& child : groupNode.children ) - writeTestCase( *child ); - - xml.scopedElement( "system-out" ).writeText( trim( stdOutForSuite ), XmlFormatting::Newline ); - xml.scopedElement( "system-err" ).writeText( trim( stdErrForSuite ), XmlFormatting::Newline ); - } - - void JunitReporter::writeTestCase( TestCaseNode const& testCaseNode ) { - TestCaseStats const& stats = testCaseNode.value; - - // All test cases have exactly one section - which represents the - // test case itself. That section may have 0-n nested sections - assert( testCaseNode.children.size() == 1 ); - SectionNode const& rootSection = *testCaseNode.children.front(); - - std::string className = stats.testInfo.className; - - if( className.empty() ) { - className = fileNameTag(stats.testInfo.tags); - if ( className.empty() ) - className = "global"; - } - - if ( !m_config->name().empty() ) - className = m_config->name() + "." + className; - - writeSection( className, "", rootSection ); - } - - void JunitReporter::writeSection( std::string const& className, - std::string const& rootName, - SectionNode const& sectionNode ) { - std::string name = trim( sectionNode.stats.sectionInfo.name ); - if( !rootName.empty() ) - name = rootName + '/' + name; - - if( !sectionNode.assertions.empty() || - !sectionNode.stdOut.empty() || - !sectionNode.stdErr.empty() ) { - XmlWriter::ScopedElement e = xml.scopedElement( "testcase" ); - if( className.empty() ) { - xml.writeAttribute( "classname", name ); - xml.writeAttribute( "name", "root" ); - } - else { - xml.writeAttribute( "classname", className ); - xml.writeAttribute( "name", name ); - } - xml.writeAttribute( "time", ::Catch::Detail::stringify( sectionNode.stats.durationInSeconds ) ); - // This is not ideal, but it should be enough to mimic gtest's - // junit output. - // Ideally the JUnit reporter would also handle `skipTest` - // events and write those out appropriately. - xml.writeAttribute( "status", "run" ); - - writeAssertions( sectionNode ); - - if( !sectionNode.stdOut.empty() ) - xml.scopedElement( "system-out" ).writeText( trim( sectionNode.stdOut ), XmlFormatting::Newline ); - if( !sectionNode.stdErr.empty() ) - xml.scopedElement( "system-err" ).writeText( trim( sectionNode.stdErr ), XmlFormatting::Newline ); - } - for( auto const& childNode : sectionNode.childSections ) - if( className.empty() ) - writeSection( name, "", *childNode ); - else - writeSection( className, name, *childNode ); - } - - void JunitReporter::writeAssertions( SectionNode const& sectionNode ) { - for( auto const& assertion : sectionNode.assertions ) - writeAssertion( assertion ); - } - - void JunitReporter::writeAssertion( AssertionStats const& stats ) { - AssertionResult const& result = stats.assertionResult; - if( !result.isOk() ) { - std::string elementName; - switch( result.getResultType() ) { - case ResultWas::ThrewException: - case ResultWas::FatalErrorCondition: - elementName = "error"; - break; - case ResultWas::ExplicitFailure: - case ResultWas::ExpressionFailed: - case ResultWas::DidntThrowException: - elementName = "failure"; - break; - - // We should never see these here: - case ResultWas::Info: - case ResultWas::Warning: - case ResultWas::Ok: - case ResultWas::Unknown: - case ResultWas::FailureBit: - case ResultWas::Exception: - elementName = "internalError"; - break; - } - - XmlWriter::ScopedElement e = xml.scopedElement( elementName ); - - xml.writeAttribute( "message", result.getExpression() ); - xml.writeAttribute( "type", result.getTestMacroName() ); - - ReusableStringStream rss; - if (stats.totals.assertions.total() > 0) { - rss << "FAILED" << ":\n"; - if (result.hasExpression()) { - rss << " "; - rss << result.getExpressionInMacro(); - rss << '\n'; - } - if (result.hasExpandedExpression()) { - rss << "with expansion:\n"; - rss << Column(result.getExpandedExpression()).indent(2) << '\n'; - } - } else { - rss << '\n'; - } - - if( !result.getMessage().empty() ) - rss << result.getMessage() << '\n'; - for( auto const& msg : stats.infoMessages ) - if( msg.type == ResultWas::Info ) - rss << msg.message << '\n'; - - rss << "at " << result.getSourceInfo(); - xml.writeText( rss.str(), XmlFormatting::Newline ); - } - } - - CATCH_REGISTER_REPORTER( "junit", JunitReporter ) - -} // end namespace Catch -// end catch_reporter_junit.cpp -// start catch_reporter_listening.cpp - -#include - -namespace Catch { - - ListeningReporter::ListeningReporter() { - // We will assume that listeners will always want all assertions - m_preferences.shouldReportAllAssertions = true; - } - - void ListeningReporter::addListener( IStreamingReporterPtr&& listener ) { - m_listeners.push_back( std::move( listener ) ); - } - - void ListeningReporter::addReporter(IStreamingReporterPtr&& reporter) { - assert(!m_reporter && "Listening reporter can wrap only 1 real reporter"); - m_reporter = std::move( reporter ); - m_preferences.shouldRedirectStdOut = m_reporter->getPreferences().shouldRedirectStdOut; - } - - ReporterPreferences ListeningReporter::getPreferences() const { - return m_preferences; - } - - std::set ListeningReporter::getSupportedVerbosities() { - return std::set{ }; - } - - void ListeningReporter::noMatchingTestCases( std::string const& spec ) { - for ( auto const& listener : m_listeners ) { - listener->noMatchingTestCases( spec ); - } - m_reporter->noMatchingTestCases( spec ); - } - - void ListeningReporter::reportInvalidArguments(std::string const&arg){ - for ( auto const& listener : m_listeners ) { - listener->reportInvalidArguments( arg ); - } - m_reporter->reportInvalidArguments( arg ); - } - -#if defined(CATCH_CONFIG_ENABLE_BENCHMARKING) - void ListeningReporter::benchmarkPreparing( std::string const& name ) { - for (auto const& listener : m_listeners) { - listener->benchmarkPreparing(name); - } - m_reporter->benchmarkPreparing(name); - } - void ListeningReporter::benchmarkStarting( BenchmarkInfo const& benchmarkInfo ) { - for ( auto const& listener : m_listeners ) { - listener->benchmarkStarting( benchmarkInfo ); - } - m_reporter->benchmarkStarting( benchmarkInfo ); - } - void ListeningReporter::benchmarkEnded( BenchmarkStats<> const& benchmarkStats ) { - for ( auto const& listener : m_listeners ) { - listener->benchmarkEnded( benchmarkStats ); - } - m_reporter->benchmarkEnded( benchmarkStats ); - } - - void ListeningReporter::benchmarkFailed( std::string const& error ) { - for (auto const& listener : m_listeners) { - listener->benchmarkFailed(error); - } - m_reporter->benchmarkFailed(error); - } -#endif // CATCH_CONFIG_ENABLE_BENCHMARKING - - void ListeningReporter::testRunStarting( TestRunInfo const& testRunInfo ) { - for ( auto const& listener : m_listeners ) { - listener->testRunStarting( testRunInfo ); - } - m_reporter->testRunStarting( testRunInfo ); - } - - void ListeningReporter::testGroupStarting( GroupInfo const& groupInfo ) { - for ( auto const& listener : m_listeners ) { - listener->testGroupStarting( groupInfo ); - } - m_reporter->testGroupStarting( groupInfo ); - } - - void ListeningReporter::testCaseStarting( TestCaseInfo const& testInfo ) { - for ( auto const& listener : m_listeners ) { - listener->testCaseStarting( testInfo ); - } - m_reporter->testCaseStarting( testInfo ); - } - - void ListeningReporter::sectionStarting( SectionInfo const& sectionInfo ) { - for ( auto const& listener : m_listeners ) { - listener->sectionStarting( sectionInfo ); - } - m_reporter->sectionStarting( sectionInfo ); - } - - void ListeningReporter::assertionStarting( AssertionInfo const& assertionInfo ) { - for ( auto const& listener : m_listeners ) { - listener->assertionStarting( assertionInfo ); - } - m_reporter->assertionStarting( assertionInfo ); - } - - // The return value indicates if the messages buffer should be cleared: - bool ListeningReporter::assertionEnded( AssertionStats const& assertionStats ) { - for( auto const& listener : m_listeners ) { - static_cast( listener->assertionEnded( assertionStats ) ); - } - return m_reporter->assertionEnded( assertionStats ); - } - - void ListeningReporter::sectionEnded( SectionStats const& sectionStats ) { - for ( auto const& listener : m_listeners ) { - listener->sectionEnded( sectionStats ); - } - m_reporter->sectionEnded( sectionStats ); - } - - void ListeningReporter::testCaseEnded( TestCaseStats const& testCaseStats ) { - for ( auto const& listener : m_listeners ) { - listener->testCaseEnded( testCaseStats ); - } - m_reporter->testCaseEnded( testCaseStats ); - } - - void ListeningReporter::testGroupEnded( TestGroupStats const& testGroupStats ) { - for ( auto const& listener : m_listeners ) { - listener->testGroupEnded( testGroupStats ); - } - m_reporter->testGroupEnded( testGroupStats ); - } - - void ListeningReporter::testRunEnded( TestRunStats const& testRunStats ) { - for ( auto const& listener : m_listeners ) { - listener->testRunEnded( testRunStats ); - } - m_reporter->testRunEnded( testRunStats ); - } - - void ListeningReporter::skipTest( TestCaseInfo const& testInfo ) { - for ( auto const& listener : m_listeners ) { - listener->skipTest( testInfo ); - } - m_reporter->skipTest( testInfo ); - } - - bool ListeningReporter::isMulti() const { - return true; - } - -} // end namespace Catch -// end catch_reporter_listening.cpp -// start catch_reporter_xml.cpp - -#if defined(_MSC_VER) -#pragma warning(push) -#pragma warning(disable:4061) // Not all labels are EXPLICITLY handled in switch - // Note that 4062 (not all labels are handled - // and default is missing) is enabled -#endif - -namespace Catch { - XmlReporter::XmlReporter( ReporterConfig const& _config ) - : StreamingReporterBase( _config ), - m_xml(_config.stream()) - { - m_reporterPrefs.shouldRedirectStdOut = true; - m_reporterPrefs.shouldReportAllAssertions = true; - } - - XmlReporter::~XmlReporter() = default; - - std::string XmlReporter::getDescription() { - return "Reports test results as an XML document"; - } - - std::string XmlReporter::getStylesheetRef() const { - return std::string(); - } - - void XmlReporter::writeSourceInfo( SourceLineInfo const& sourceInfo ) { - m_xml - .writeAttribute( "filename", sourceInfo.file ) - .writeAttribute( "line", sourceInfo.line ); - } - - void XmlReporter::noMatchingTestCases( std::string const& s ) { - StreamingReporterBase::noMatchingTestCases( s ); - } - - void XmlReporter::testRunStarting( TestRunInfo const& testInfo ) { - StreamingReporterBase::testRunStarting( testInfo ); - std::string stylesheetRef = getStylesheetRef(); - if( !stylesheetRef.empty() ) - m_xml.writeStylesheetRef( stylesheetRef ); - m_xml.startElement( "Catch" ); - if( !m_config->name().empty() ) - m_xml.writeAttribute( "name", m_config->name() ); - if (m_config->testSpec().hasFilters()) - m_xml.writeAttribute( "filters", serializeFilters( m_config->getTestsOrTags() ) ); - if( m_config->rngSeed() != 0 ) - m_xml.scopedElement( "Randomness" ) - .writeAttribute( "seed", m_config->rngSeed() ); - } - - void XmlReporter::testGroupStarting( GroupInfo const& groupInfo ) { - StreamingReporterBase::testGroupStarting( groupInfo ); - m_xml.startElement( "Group" ) - .writeAttribute( "name", groupInfo.name ); - } - - void XmlReporter::testCaseStarting( TestCaseInfo const& testInfo ) { - StreamingReporterBase::testCaseStarting(testInfo); - m_xml.startElement( "TestCase" ) - .writeAttribute( "name", trim( testInfo.name ) ) - .writeAttribute( "description", testInfo.description ) - .writeAttribute( "tags", testInfo.tagsAsString() ); - - writeSourceInfo( testInfo.lineInfo ); - - if ( m_config->showDurations() == ShowDurations::Always ) - m_testCaseTimer.start(); - m_xml.ensureTagClosed(); - } - - void XmlReporter::sectionStarting( SectionInfo const& sectionInfo ) { - StreamingReporterBase::sectionStarting( sectionInfo ); - if( m_sectionDepth++ > 0 ) { - m_xml.startElement( "Section" ) - .writeAttribute( "name", trim( sectionInfo.name ) ); - writeSourceInfo( sectionInfo.lineInfo ); - m_xml.ensureTagClosed(); - } - } - - void XmlReporter::assertionStarting( AssertionInfo const& ) { } - - bool XmlReporter::assertionEnded( AssertionStats const& assertionStats ) { - - AssertionResult const& result = assertionStats.assertionResult; - - bool includeResults = m_config->includeSuccessfulResults() || !result.isOk(); - - if( includeResults || result.getResultType() == ResultWas::Warning ) { - // Print any info messages in tags. - for( auto const& msg : assertionStats.infoMessages ) { - if( msg.type == ResultWas::Info && includeResults ) { - m_xml.scopedElement( "Info" ) - .writeText( msg.message ); - } else if ( msg.type == ResultWas::Warning ) { - m_xml.scopedElement( "Warning" ) - .writeText( msg.message ); - } - } - } - - // Drop out if result was successful but we're not printing them. - if( !includeResults && result.getResultType() != ResultWas::Warning ) - return true; - - // Print the expression if there is one. - if( result.hasExpression() ) { - m_xml.startElement( "Expression" ) - .writeAttribute( "success", result.succeeded() ) - .writeAttribute( "type", result.getTestMacroName() ); - - writeSourceInfo( result.getSourceInfo() ); - - m_xml.scopedElement( "Original" ) - .writeText( result.getExpression() ); - m_xml.scopedElement( "Expanded" ) - .writeText( result.getExpandedExpression() ); - } - - // And... Print a result applicable to each result type. - switch( result.getResultType() ) { - case ResultWas::ThrewException: - m_xml.startElement( "Exception" ); - writeSourceInfo( result.getSourceInfo() ); - m_xml.writeText( result.getMessage() ); - m_xml.endElement(); - break; - case ResultWas::FatalErrorCondition: - m_xml.startElement( "FatalErrorCondition" ); - writeSourceInfo( result.getSourceInfo() ); - m_xml.writeText( result.getMessage() ); - m_xml.endElement(); - break; - case ResultWas::Info: - m_xml.scopedElement( "Info" ) - .writeText( result.getMessage() ); - break; - case ResultWas::Warning: - // Warning will already have been written - break; - case ResultWas::ExplicitFailure: - m_xml.startElement( "Failure" ); - writeSourceInfo( result.getSourceInfo() ); - m_xml.writeText( result.getMessage() ); - m_xml.endElement(); - break; - default: - break; - } - - if( result.hasExpression() ) - m_xml.endElement(); - - return true; - } - - void XmlReporter::sectionEnded( SectionStats const& sectionStats ) { - StreamingReporterBase::sectionEnded( sectionStats ); - if( --m_sectionDepth > 0 ) { - XmlWriter::ScopedElement e = m_xml.scopedElement( "OverallResults" ); - e.writeAttribute( "successes", sectionStats.assertions.passed ); - e.writeAttribute( "failures", sectionStats.assertions.failed ); - e.writeAttribute( "expectedFailures", sectionStats.assertions.failedButOk ); - - if ( m_config->showDurations() == ShowDurations::Always ) - e.writeAttribute( "durationInSeconds", sectionStats.durationInSeconds ); - - m_xml.endElement(); - } - } - - void XmlReporter::testCaseEnded( TestCaseStats const& testCaseStats ) { - StreamingReporterBase::testCaseEnded( testCaseStats ); - XmlWriter::ScopedElement e = m_xml.scopedElement( "OverallResult" ); - e.writeAttribute( "success", testCaseStats.totals.assertions.allOk() ); - - if ( m_config->showDurations() == ShowDurations::Always ) - e.writeAttribute( "durationInSeconds", m_testCaseTimer.getElapsedSeconds() ); - - if( !testCaseStats.stdOut.empty() ) - m_xml.scopedElement( "StdOut" ).writeText( trim( testCaseStats.stdOut ), XmlFormatting::Newline ); - if( !testCaseStats.stdErr.empty() ) - m_xml.scopedElement( "StdErr" ).writeText( trim( testCaseStats.stdErr ), XmlFormatting::Newline ); - - m_xml.endElement(); - } - - void XmlReporter::testGroupEnded( TestGroupStats const& testGroupStats ) { - StreamingReporterBase::testGroupEnded( testGroupStats ); - // TODO: Check testGroupStats.aborting and act accordingly. - m_xml.scopedElement( "OverallResults" ) - .writeAttribute( "successes", testGroupStats.totals.assertions.passed ) - .writeAttribute( "failures", testGroupStats.totals.assertions.failed ) - .writeAttribute( "expectedFailures", testGroupStats.totals.assertions.failedButOk ); - m_xml.scopedElement( "OverallResultsCases") - .writeAttribute( "successes", testGroupStats.totals.testCases.passed ) - .writeAttribute( "failures", testGroupStats.totals.testCases.failed ) - .writeAttribute( "expectedFailures", testGroupStats.totals.testCases.failedButOk ); - m_xml.endElement(); - } - - void XmlReporter::testRunEnded( TestRunStats const& testRunStats ) { - StreamingReporterBase::testRunEnded( testRunStats ); - m_xml.scopedElement( "OverallResults" ) - .writeAttribute( "successes", testRunStats.totals.assertions.passed ) - .writeAttribute( "failures", testRunStats.totals.assertions.failed ) - .writeAttribute( "expectedFailures", testRunStats.totals.assertions.failedButOk ); - m_xml.scopedElement( "OverallResultsCases") - .writeAttribute( "successes", testRunStats.totals.testCases.passed ) - .writeAttribute( "failures", testRunStats.totals.testCases.failed ) - .writeAttribute( "expectedFailures", testRunStats.totals.testCases.failedButOk ); - m_xml.endElement(); - } - -#if defined(CATCH_CONFIG_ENABLE_BENCHMARKING) - void XmlReporter::benchmarkPreparing(std::string const& name) { - m_xml.startElement("BenchmarkResults") - .writeAttribute("name", name); - } - - void XmlReporter::benchmarkStarting(BenchmarkInfo const &info) { - m_xml.writeAttribute("samples", info.samples) - .writeAttribute("resamples", info.resamples) - .writeAttribute("iterations", info.iterations) - .writeAttribute("clockResolution", info.clockResolution) - .writeAttribute("estimatedDuration", info.estimatedDuration) - .writeComment("All values in nano seconds"); - } - - void XmlReporter::benchmarkEnded(BenchmarkStats<> const& benchmarkStats) { - m_xml.startElement("mean") - .writeAttribute("value", benchmarkStats.mean.point.count()) - .writeAttribute("lowerBound", benchmarkStats.mean.lower_bound.count()) - .writeAttribute("upperBound", benchmarkStats.mean.upper_bound.count()) - .writeAttribute("ci", benchmarkStats.mean.confidence_interval); - m_xml.endElement(); - m_xml.startElement("standardDeviation") - .writeAttribute("value", benchmarkStats.standardDeviation.point.count()) - .writeAttribute("lowerBound", benchmarkStats.standardDeviation.lower_bound.count()) - .writeAttribute("upperBound", benchmarkStats.standardDeviation.upper_bound.count()) - .writeAttribute("ci", benchmarkStats.standardDeviation.confidence_interval); - m_xml.endElement(); - m_xml.startElement("outliers") - .writeAttribute("variance", benchmarkStats.outlierVariance) - .writeAttribute("lowMild", benchmarkStats.outliers.low_mild) - .writeAttribute("lowSevere", benchmarkStats.outliers.low_severe) - .writeAttribute("highMild", benchmarkStats.outliers.high_mild) - .writeAttribute("highSevere", benchmarkStats.outliers.high_severe); - m_xml.endElement(); - m_xml.endElement(); - } - - void XmlReporter::benchmarkFailed(std::string const &error) { - m_xml.scopedElement("failed"). - writeAttribute("message", error); - m_xml.endElement(); - } -#endif // CATCH_CONFIG_ENABLE_BENCHMARKING - - CATCH_REGISTER_REPORTER( "xml", XmlReporter ) - -} // end namespace Catch - -#if defined(_MSC_VER) -#pragma warning(pop) -#endif -// end catch_reporter_xml.cpp - -namespace Catch { - LeakDetector leakDetector; -} - -#ifdef __clang__ -#pragma clang diagnostic pop -#endif - -// end catch_impl.hpp -#endif - -#ifdef CATCH_CONFIG_MAIN -// start catch_default_main.hpp - -#ifndef __OBJC__ - -#if defined(CATCH_CONFIG_WCHAR) && defined(CATCH_PLATFORM_WINDOWS) && defined(_UNICODE) && !defined(DO_NOT_USE_WMAIN) -// Standard C/C++ Win32 Unicode wmain entry point -extern "C" int wmain (int argc, wchar_t * argv[], wchar_t * []) { -#else -// Standard C/C++ main entry point -int main (int argc, char * argv[]) { -#endif - - return Catch::Session().run( argc, argv ); -} - -#else // __OBJC__ - -// Objective-C entry point -int main (int argc, char * const argv[]) { -#if !CATCH_ARC_ENABLED - NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; -#endif - - Catch::registerTestMethods(); - int result = Catch::Session().run( argc, (char**)argv ); - -#if !CATCH_ARC_ENABLED - [pool drain]; -#endif - - return result; -} - -#endif // __OBJC__ - -// end catch_default_main.hpp -#endif - -#if !defined(CATCH_CONFIG_IMPL_ONLY) - -#ifdef CLARA_CONFIG_MAIN_NOT_DEFINED -# undef CLARA_CONFIG_MAIN -#endif - -#if !defined(CATCH_CONFIG_DISABLE) -////// -// If this config identifier is defined then all CATCH macros are prefixed with CATCH_ -#ifdef CATCH_CONFIG_PREFIX_ALL - -#define CATCH_REQUIRE( ... ) INTERNAL_CATCH_TEST( "CATCH_REQUIRE", Catch::ResultDisposition::Normal, __VA_ARGS__ ) -#define CATCH_REQUIRE_FALSE( ... ) INTERNAL_CATCH_TEST( "CATCH_REQUIRE_FALSE", Catch::ResultDisposition::Normal | Catch::ResultDisposition::FalseTest, __VA_ARGS__ ) - -#define CATCH_REQUIRE_THROWS( ... ) INTERNAL_CATCH_THROWS( "CATCH_REQUIRE_THROWS", Catch::ResultDisposition::Normal, __VA_ARGS__ ) -#define CATCH_REQUIRE_THROWS_AS( expr, exceptionType ) INTERNAL_CATCH_THROWS_AS( "CATCH_REQUIRE_THROWS_AS", exceptionType, Catch::ResultDisposition::Normal, expr ) -#define CATCH_REQUIRE_THROWS_WITH( expr, matcher ) INTERNAL_CATCH_THROWS_STR_MATCHES( "CATCH_REQUIRE_THROWS_WITH", Catch::ResultDisposition::Normal, matcher, expr ) -#if !defined(CATCH_CONFIG_DISABLE_MATCHERS) -#define CATCH_REQUIRE_THROWS_MATCHES( expr, exceptionType, matcher ) INTERNAL_CATCH_THROWS_MATCHES( "CATCH_REQUIRE_THROWS_MATCHES", exceptionType, Catch::ResultDisposition::Normal, matcher, expr ) -#endif// CATCH_CONFIG_DISABLE_MATCHERS -#define CATCH_REQUIRE_NOTHROW( ... ) INTERNAL_CATCH_NO_THROW( "CATCH_REQUIRE_NOTHROW", Catch::ResultDisposition::Normal, __VA_ARGS__ ) - -#define CATCH_CHECK( ... ) INTERNAL_CATCH_TEST( "CATCH_CHECK", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ ) -#define CATCH_CHECK_FALSE( ... ) INTERNAL_CATCH_TEST( "CATCH_CHECK_FALSE", Catch::ResultDisposition::ContinueOnFailure | Catch::ResultDisposition::FalseTest, __VA_ARGS__ ) -#define CATCH_CHECKED_IF( ... ) INTERNAL_CATCH_IF( "CATCH_CHECKED_IF", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ ) -#define CATCH_CHECKED_ELSE( ... ) INTERNAL_CATCH_ELSE( "CATCH_CHECKED_ELSE", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ ) -#define CATCH_CHECK_NOFAIL( ... ) INTERNAL_CATCH_TEST( "CATCH_CHECK_NOFAIL", Catch::ResultDisposition::ContinueOnFailure | Catch::ResultDisposition::SuppressFail, __VA_ARGS__ ) - -#define CATCH_CHECK_THROWS( ... ) INTERNAL_CATCH_THROWS( "CATCH_CHECK_THROWS", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ ) -#define CATCH_CHECK_THROWS_AS( expr, exceptionType ) INTERNAL_CATCH_THROWS_AS( "CATCH_CHECK_THROWS_AS", exceptionType, Catch::ResultDisposition::ContinueOnFailure, expr ) -#define CATCH_CHECK_THROWS_WITH( expr, matcher ) INTERNAL_CATCH_THROWS_STR_MATCHES( "CATCH_CHECK_THROWS_WITH", Catch::ResultDisposition::ContinueOnFailure, matcher, expr ) -#if !defined(CATCH_CONFIG_DISABLE_MATCHERS) -#define CATCH_CHECK_THROWS_MATCHES( expr, exceptionType, matcher ) INTERNAL_CATCH_THROWS_MATCHES( "CATCH_CHECK_THROWS_MATCHES", exceptionType, Catch::ResultDisposition::ContinueOnFailure, matcher, expr ) -#endif // CATCH_CONFIG_DISABLE_MATCHERS -#define CATCH_CHECK_NOTHROW( ... ) INTERNAL_CATCH_NO_THROW( "CATCH_CHECK_NOTHROW", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ ) - -#if !defined(CATCH_CONFIG_DISABLE_MATCHERS) -#define CATCH_CHECK_THAT( arg, matcher ) INTERNAL_CHECK_THAT( "CATCH_CHECK_THAT", matcher, Catch::ResultDisposition::ContinueOnFailure, arg ) - -#define CATCH_REQUIRE_THAT( arg, matcher ) INTERNAL_CHECK_THAT( "CATCH_REQUIRE_THAT", matcher, Catch::ResultDisposition::Normal, arg ) -#endif // CATCH_CONFIG_DISABLE_MATCHERS - -#define CATCH_INFO( msg ) INTERNAL_CATCH_INFO( "CATCH_INFO", msg ) -#define CATCH_UNSCOPED_INFO( msg ) INTERNAL_CATCH_UNSCOPED_INFO( "CATCH_UNSCOPED_INFO", msg ) -#define CATCH_WARN( msg ) INTERNAL_CATCH_MSG( "CATCH_WARN", Catch::ResultWas::Warning, Catch::ResultDisposition::ContinueOnFailure, msg ) -#define CATCH_CAPTURE( ... ) INTERNAL_CATCH_CAPTURE( INTERNAL_CATCH_UNIQUE_NAME(capturer), "CATCH_CAPTURE",__VA_ARGS__ ) - -#define CATCH_TEST_CASE( ... ) INTERNAL_CATCH_TESTCASE( __VA_ARGS__ ) -#define CATCH_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_TEST_CASE_METHOD( className, __VA_ARGS__ ) -#define CATCH_METHOD_AS_TEST_CASE( method, ... ) INTERNAL_CATCH_METHOD_AS_TEST_CASE( method, __VA_ARGS__ ) -#define CATCH_REGISTER_TEST_CASE( Function, ... ) INTERNAL_CATCH_REGISTER_TESTCASE( Function, __VA_ARGS__ ) -#define CATCH_SECTION( ... ) INTERNAL_CATCH_SECTION( __VA_ARGS__ ) -#define CATCH_DYNAMIC_SECTION( ... ) INTERNAL_CATCH_DYNAMIC_SECTION( __VA_ARGS__ ) -#define CATCH_FAIL( ... ) INTERNAL_CATCH_MSG( "CATCH_FAIL", Catch::ResultWas::ExplicitFailure, Catch::ResultDisposition::Normal, __VA_ARGS__ ) -#define CATCH_FAIL_CHECK( ... ) INTERNAL_CATCH_MSG( "CATCH_FAIL_CHECK", Catch::ResultWas::ExplicitFailure, Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ ) -#define CATCH_SUCCEED( ... ) INTERNAL_CATCH_MSG( "CATCH_SUCCEED", Catch::ResultWas::Ok, Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ ) - -#define CATCH_ANON_TEST_CASE() INTERNAL_CATCH_TESTCASE() - -#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR -#define CATCH_TEMPLATE_TEST_CASE( ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE( __VA_ARGS__ ) -#define CATCH_TEMPLATE_TEST_CASE_SIG( ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE_SIG( __VA_ARGS__ ) -#define CATCH_TEMPLATE_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD( className, __VA_ARGS__ ) -#define CATCH_TEMPLATE_TEST_CASE_METHOD_SIG( className, ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_SIG( className, __VA_ARGS__ ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE( ... ) INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE( __VA_ARGS__ ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_SIG( ... ) INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_SIG( __VA_ARGS__ ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD( className, __VA_ARGS__ ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG( className, ... ) INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG( className, __VA_ARGS__ ) -#else -#define CATCH_TEMPLATE_TEST_CASE( ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE( __VA_ARGS__ ) ) -#define CATCH_TEMPLATE_TEST_CASE_SIG( ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE_SIG( __VA_ARGS__ ) ) -#define CATCH_TEMPLATE_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD( className, __VA_ARGS__ ) ) -#define CATCH_TEMPLATE_TEST_CASE_METHOD_SIG( className, ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_SIG( className, __VA_ARGS__ ) ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE( ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE( __VA_ARGS__ ) ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_SIG( ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_SIG( __VA_ARGS__ ) ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD( className, __VA_ARGS__ ) ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG( className, ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG( className, __VA_ARGS__ ) ) -#endif - -#if !defined(CATCH_CONFIG_RUNTIME_STATIC_REQUIRE) -#define CATCH_STATIC_REQUIRE( ... ) static_assert( __VA_ARGS__ , #__VA_ARGS__ ); CATCH_SUCCEED( #__VA_ARGS__ ) -#define CATCH_STATIC_REQUIRE_FALSE( ... ) static_assert( !(__VA_ARGS__), "!(" #__VA_ARGS__ ")" ); CATCH_SUCCEED( #__VA_ARGS__ ) -#else -#define CATCH_STATIC_REQUIRE( ... ) CATCH_REQUIRE( __VA_ARGS__ ) -#define CATCH_STATIC_REQUIRE_FALSE( ... ) CATCH_REQUIRE_FALSE( __VA_ARGS__ ) -#endif - -// "BDD-style" convenience wrappers -#define CATCH_SCENARIO( ... ) CATCH_TEST_CASE( "Scenario: " __VA_ARGS__ ) -#define CATCH_SCENARIO_METHOD( className, ... ) INTERNAL_CATCH_TEST_CASE_METHOD( className, "Scenario: " __VA_ARGS__ ) -#define CATCH_GIVEN( desc ) INTERNAL_CATCH_DYNAMIC_SECTION( " Given: " << desc ) -#define CATCH_AND_GIVEN( desc ) INTERNAL_CATCH_DYNAMIC_SECTION( "And given: " << desc ) -#define CATCH_WHEN( desc ) INTERNAL_CATCH_DYNAMIC_SECTION( " When: " << desc ) -#define CATCH_AND_WHEN( desc ) INTERNAL_CATCH_DYNAMIC_SECTION( " And when: " << desc ) -#define CATCH_THEN( desc ) INTERNAL_CATCH_DYNAMIC_SECTION( " Then: " << desc ) -#define CATCH_AND_THEN( desc ) INTERNAL_CATCH_DYNAMIC_SECTION( " And: " << desc ) - -#if defined(CATCH_CONFIG_ENABLE_BENCHMARKING) -#define CATCH_BENCHMARK(...) \ - INTERNAL_CATCH_BENCHMARK(INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____B_E_N_C_H____), INTERNAL_CATCH_GET_1_ARG(__VA_ARGS__,,), INTERNAL_CATCH_GET_2_ARG(__VA_ARGS__,,)) -#define CATCH_BENCHMARK_ADVANCED(name) \ - INTERNAL_CATCH_BENCHMARK_ADVANCED(INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____B_E_N_C_H____), name) -#endif // CATCH_CONFIG_ENABLE_BENCHMARKING - -// If CATCH_CONFIG_PREFIX_ALL is not defined then the CATCH_ prefix is not required -#else - -#define REQUIRE( ... ) INTERNAL_CATCH_TEST( "REQUIRE", Catch::ResultDisposition::Normal, __VA_ARGS__ ) -#define REQUIRE_FALSE( ... ) INTERNAL_CATCH_TEST( "REQUIRE_FALSE", Catch::ResultDisposition::Normal | Catch::ResultDisposition::FalseTest, __VA_ARGS__ ) - -#define REQUIRE_THROWS( ... ) INTERNAL_CATCH_THROWS( "REQUIRE_THROWS", Catch::ResultDisposition::Normal, __VA_ARGS__ ) -#define REQUIRE_THROWS_AS( expr, exceptionType ) INTERNAL_CATCH_THROWS_AS( "REQUIRE_THROWS_AS", exceptionType, Catch::ResultDisposition::Normal, expr ) -#define REQUIRE_THROWS_WITH( expr, matcher ) INTERNAL_CATCH_THROWS_STR_MATCHES( "REQUIRE_THROWS_WITH", Catch::ResultDisposition::Normal, matcher, expr ) -#if !defined(CATCH_CONFIG_DISABLE_MATCHERS) -#define REQUIRE_THROWS_MATCHES( expr, exceptionType, matcher ) INTERNAL_CATCH_THROWS_MATCHES( "REQUIRE_THROWS_MATCHES", exceptionType, Catch::ResultDisposition::Normal, matcher, expr ) -#endif // CATCH_CONFIG_DISABLE_MATCHERS -#define REQUIRE_NOTHROW( ... ) INTERNAL_CATCH_NO_THROW( "REQUIRE_NOTHROW", Catch::ResultDisposition::Normal, __VA_ARGS__ ) - -#define CHECK( ... ) INTERNAL_CATCH_TEST( "CHECK", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ ) -#define CHECK_FALSE( ... ) INTERNAL_CATCH_TEST( "CHECK_FALSE", Catch::ResultDisposition::ContinueOnFailure | Catch::ResultDisposition::FalseTest, __VA_ARGS__ ) -#define CHECKED_IF( ... ) INTERNAL_CATCH_IF( "CHECKED_IF", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ ) -#define CHECKED_ELSE( ... ) INTERNAL_CATCH_ELSE( "CHECKED_ELSE", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ ) -#define CHECK_NOFAIL( ... ) INTERNAL_CATCH_TEST( "CHECK_NOFAIL", Catch::ResultDisposition::ContinueOnFailure | Catch::ResultDisposition::SuppressFail, __VA_ARGS__ ) - -#define CHECK_THROWS( ... ) INTERNAL_CATCH_THROWS( "CHECK_THROWS", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ ) -#define CHECK_THROWS_AS( expr, exceptionType ) INTERNAL_CATCH_THROWS_AS( "CHECK_THROWS_AS", exceptionType, Catch::ResultDisposition::ContinueOnFailure, expr ) -#define CHECK_THROWS_WITH( expr, matcher ) INTERNAL_CATCH_THROWS_STR_MATCHES( "CHECK_THROWS_WITH", Catch::ResultDisposition::ContinueOnFailure, matcher, expr ) -#if !defined(CATCH_CONFIG_DISABLE_MATCHERS) -#define CHECK_THROWS_MATCHES( expr, exceptionType, matcher ) INTERNAL_CATCH_THROWS_MATCHES( "CHECK_THROWS_MATCHES", exceptionType, Catch::ResultDisposition::ContinueOnFailure, matcher, expr ) -#endif // CATCH_CONFIG_DISABLE_MATCHERS -#define CHECK_NOTHROW( ... ) INTERNAL_CATCH_NO_THROW( "CHECK_NOTHROW", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ ) - -#if !defined(CATCH_CONFIG_DISABLE_MATCHERS) -#define CHECK_THAT( arg, matcher ) INTERNAL_CHECK_THAT( "CHECK_THAT", matcher, Catch::ResultDisposition::ContinueOnFailure, arg ) - -#define REQUIRE_THAT( arg, matcher ) INTERNAL_CHECK_THAT( "REQUIRE_THAT", matcher, Catch::ResultDisposition::Normal, arg ) -#endif // CATCH_CONFIG_DISABLE_MATCHERS - -#define INFO( msg ) INTERNAL_CATCH_INFO( "INFO", msg ) -#define UNSCOPED_INFO( msg ) INTERNAL_CATCH_UNSCOPED_INFO( "UNSCOPED_INFO", msg ) -#define WARN( msg ) INTERNAL_CATCH_MSG( "WARN", Catch::ResultWas::Warning, Catch::ResultDisposition::ContinueOnFailure, msg ) -#define CAPTURE( ... ) INTERNAL_CATCH_CAPTURE( INTERNAL_CATCH_UNIQUE_NAME(capturer), "CAPTURE",__VA_ARGS__ ) - -#define TEST_CASE( ... ) INTERNAL_CATCH_TESTCASE( __VA_ARGS__ ) -#define TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_TEST_CASE_METHOD( className, __VA_ARGS__ ) -#define METHOD_AS_TEST_CASE( method, ... ) INTERNAL_CATCH_METHOD_AS_TEST_CASE( method, __VA_ARGS__ ) -#define REGISTER_TEST_CASE( Function, ... ) INTERNAL_CATCH_REGISTER_TESTCASE( Function, __VA_ARGS__ ) -#define SECTION( ... ) INTERNAL_CATCH_SECTION( __VA_ARGS__ ) -#define DYNAMIC_SECTION( ... ) INTERNAL_CATCH_DYNAMIC_SECTION( __VA_ARGS__ ) -#define FAIL( ... ) INTERNAL_CATCH_MSG( "FAIL", Catch::ResultWas::ExplicitFailure, Catch::ResultDisposition::Normal, __VA_ARGS__ ) -#define FAIL_CHECK( ... ) INTERNAL_CATCH_MSG( "FAIL_CHECK", Catch::ResultWas::ExplicitFailure, Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ ) -#define SUCCEED( ... ) INTERNAL_CATCH_MSG( "SUCCEED", Catch::ResultWas::Ok, Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__ ) -#define ANON_TEST_CASE() INTERNAL_CATCH_TESTCASE() - -#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR -#define TEMPLATE_TEST_CASE( ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE( __VA_ARGS__ ) -#define TEMPLATE_TEST_CASE_SIG( ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE_SIG( __VA_ARGS__ ) -#define TEMPLATE_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD( className, __VA_ARGS__ ) -#define TEMPLATE_TEST_CASE_METHOD_SIG( className, ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_SIG( className, __VA_ARGS__ ) -#define TEMPLATE_PRODUCT_TEST_CASE( ... ) INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE( __VA_ARGS__ ) -#define TEMPLATE_PRODUCT_TEST_CASE_SIG( ... ) INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_SIG( __VA_ARGS__ ) -#define TEMPLATE_PRODUCT_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD( className, __VA_ARGS__ ) -#define TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG( className, ... ) INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG( className, __VA_ARGS__ ) -#define TEMPLATE_LIST_TEST_CASE( ... ) INTERNAL_CATCH_TEMPLATE_LIST_TEST_CASE(__VA_ARGS__) -#define TEMPLATE_LIST_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_TEMPLATE_LIST_TEST_CASE_METHOD( className, __VA_ARGS__ ) -#else -#define TEMPLATE_TEST_CASE( ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE( __VA_ARGS__ ) ) -#define TEMPLATE_TEST_CASE_SIG( ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE_SIG( __VA_ARGS__ ) ) -#define TEMPLATE_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD( className, __VA_ARGS__ ) ) -#define TEMPLATE_TEST_CASE_METHOD_SIG( className, ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_SIG( className, __VA_ARGS__ ) ) -#define TEMPLATE_PRODUCT_TEST_CASE( ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE( __VA_ARGS__ ) ) -#define TEMPLATE_PRODUCT_TEST_CASE_SIG( ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_SIG( __VA_ARGS__ ) ) -#define TEMPLATE_PRODUCT_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD( className, __VA_ARGS__ ) ) -#define TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG( className, ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG( className, __VA_ARGS__ ) ) -#define TEMPLATE_LIST_TEST_CASE( ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_LIST_TEST_CASE( __VA_ARGS__ ) ) -#define TEMPLATE_LIST_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_LIST_TEST_CASE_METHOD( className, __VA_ARGS__ ) ) -#endif - -#if !defined(CATCH_CONFIG_RUNTIME_STATIC_REQUIRE) -#define STATIC_REQUIRE( ... ) static_assert( __VA_ARGS__, #__VA_ARGS__ ); SUCCEED( #__VA_ARGS__ ) -#define STATIC_REQUIRE_FALSE( ... ) static_assert( !(__VA_ARGS__), "!(" #__VA_ARGS__ ")" ); SUCCEED( "!(" #__VA_ARGS__ ")" ) -#else -#define STATIC_REQUIRE( ... ) REQUIRE( __VA_ARGS__ ) -#define STATIC_REQUIRE_FALSE( ... ) REQUIRE_FALSE( __VA_ARGS__ ) -#endif - -#endif - -#define CATCH_TRANSLATE_EXCEPTION( signature ) INTERNAL_CATCH_TRANSLATE_EXCEPTION( signature ) - -// "BDD-style" convenience wrappers -#define SCENARIO( ... ) TEST_CASE( "Scenario: " __VA_ARGS__ ) -#define SCENARIO_METHOD( className, ... ) INTERNAL_CATCH_TEST_CASE_METHOD( className, "Scenario: " __VA_ARGS__ ) - -#define GIVEN( desc ) INTERNAL_CATCH_DYNAMIC_SECTION( " Given: " << desc ) -#define AND_GIVEN( desc ) INTERNAL_CATCH_DYNAMIC_SECTION( "And given: " << desc ) -#define WHEN( desc ) INTERNAL_CATCH_DYNAMIC_SECTION( " When: " << desc ) -#define AND_WHEN( desc ) INTERNAL_CATCH_DYNAMIC_SECTION( " And when: " << desc ) -#define THEN( desc ) INTERNAL_CATCH_DYNAMIC_SECTION( " Then: " << desc ) -#define AND_THEN( desc ) INTERNAL_CATCH_DYNAMIC_SECTION( " And: " << desc ) - -#if defined(CATCH_CONFIG_ENABLE_BENCHMARKING) -#define BENCHMARK(...) \ - INTERNAL_CATCH_BENCHMARK(INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____B_E_N_C_H____), INTERNAL_CATCH_GET_1_ARG(__VA_ARGS__,,), INTERNAL_CATCH_GET_2_ARG(__VA_ARGS__,,)) -#define BENCHMARK_ADVANCED(name) \ - INTERNAL_CATCH_BENCHMARK_ADVANCED(INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____B_E_N_C_H____), name) -#endif // CATCH_CONFIG_ENABLE_BENCHMARKING - -using Catch::Detail::Approx; - -#else // CATCH_CONFIG_DISABLE - -////// -// If this config identifier is defined then all CATCH macros are prefixed with CATCH_ -#ifdef CATCH_CONFIG_PREFIX_ALL - -#define CATCH_REQUIRE( ... ) (void)(0) -#define CATCH_REQUIRE_FALSE( ... ) (void)(0) - -#define CATCH_REQUIRE_THROWS( ... ) (void)(0) -#define CATCH_REQUIRE_THROWS_AS( expr, exceptionType ) (void)(0) -#define CATCH_REQUIRE_THROWS_WITH( expr, matcher ) (void)(0) -#if !defined(CATCH_CONFIG_DISABLE_MATCHERS) -#define CATCH_REQUIRE_THROWS_MATCHES( expr, exceptionType, matcher ) (void)(0) -#endif// CATCH_CONFIG_DISABLE_MATCHERS -#define CATCH_REQUIRE_NOTHROW( ... ) (void)(0) - -#define CATCH_CHECK( ... ) (void)(0) -#define CATCH_CHECK_FALSE( ... ) (void)(0) -#define CATCH_CHECKED_IF( ... ) if (__VA_ARGS__) -#define CATCH_CHECKED_ELSE( ... ) if (!(__VA_ARGS__)) -#define CATCH_CHECK_NOFAIL( ... ) (void)(0) - -#define CATCH_CHECK_THROWS( ... ) (void)(0) -#define CATCH_CHECK_THROWS_AS( expr, exceptionType ) (void)(0) -#define CATCH_CHECK_THROWS_WITH( expr, matcher ) (void)(0) -#if !defined(CATCH_CONFIG_DISABLE_MATCHERS) -#define CATCH_CHECK_THROWS_MATCHES( expr, exceptionType, matcher ) (void)(0) -#endif // CATCH_CONFIG_DISABLE_MATCHERS -#define CATCH_CHECK_NOTHROW( ... ) (void)(0) - -#if !defined(CATCH_CONFIG_DISABLE_MATCHERS) -#define CATCH_CHECK_THAT( arg, matcher ) (void)(0) - -#define CATCH_REQUIRE_THAT( arg, matcher ) (void)(0) -#endif // CATCH_CONFIG_DISABLE_MATCHERS - -#define CATCH_INFO( msg ) (void)(0) -#define CATCH_UNSCOPED_INFO( msg ) (void)(0) -#define CATCH_WARN( msg ) (void)(0) -#define CATCH_CAPTURE( msg ) (void)(0) - -#define CATCH_TEST_CASE( ... ) INTERNAL_CATCH_TESTCASE_NO_REGISTRATION(INTERNAL_CATCH_UNIQUE_NAME( ____C_A_T_C_H____T_E_S_T____ )) -#define CATCH_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_TESTCASE_NO_REGISTRATION(INTERNAL_CATCH_UNIQUE_NAME( ____C_A_T_C_H____T_E_S_T____ )) -#define CATCH_METHOD_AS_TEST_CASE( method, ... ) -#define CATCH_REGISTER_TEST_CASE( Function, ... ) (void)(0) -#define CATCH_SECTION( ... ) -#define CATCH_DYNAMIC_SECTION( ... ) -#define CATCH_FAIL( ... ) (void)(0) -#define CATCH_FAIL_CHECK( ... ) (void)(0) -#define CATCH_SUCCEED( ... ) (void)(0) - -#define CATCH_ANON_TEST_CASE() INTERNAL_CATCH_TESTCASE_NO_REGISTRATION(INTERNAL_CATCH_UNIQUE_NAME( ____C_A_T_C_H____T_E_S_T____ )) - -#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR -#define CATCH_TEMPLATE_TEST_CASE( ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE_NO_REGISTRATION(__VA_ARGS__) -#define CATCH_TEMPLATE_TEST_CASE_SIG( ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE_SIG_NO_REGISTRATION(__VA_ARGS__) -#define CATCH_TEMPLATE_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_NO_REGISTRATION(className, __VA_ARGS__) -#define CATCH_TEMPLATE_TEST_CASE_METHOD_SIG( className, ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_SIG_NO_REGISTRATION(className, __VA_ARGS__ ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE( ... ) CATCH_TEMPLATE_TEST_CASE( __VA_ARGS__ ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_SIG( ... ) CATCH_TEMPLATE_TEST_CASE( __VA_ARGS__ ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD( className, ... ) CATCH_TEMPLATE_TEST_CASE_METHOD( className, __VA_ARGS__ ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG( className, ... ) CATCH_TEMPLATE_TEST_CASE_METHOD( className, __VA_ARGS__ ) -#else -#define CATCH_TEMPLATE_TEST_CASE( ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE_NO_REGISTRATION(__VA_ARGS__) ) -#define CATCH_TEMPLATE_TEST_CASE_SIG( ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE_SIG_NO_REGISTRATION(__VA_ARGS__) ) -#define CATCH_TEMPLATE_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_NO_REGISTRATION(className, __VA_ARGS__ ) ) -#define CATCH_TEMPLATE_TEST_CASE_METHOD_SIG( className, ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_SIG_NO_REGISTRATION(className, __VA_ARGS__ ) ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE( ... ) CATCH_TEMPLATE_TEST_CASE( __VA_ARGS__ ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_SIG( ... ) CATCH_TEMPLATE_TEST_CASE( __VA_ARGS__ ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD( className, ... ) CATCH_TEMPLATE_TEST_CASE_METHOD( className, __VA_ARGS__ ) -#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG( className, ... ) CATCH_TEMPLATE_TEST_CASE_METHOD( className, __VA_ARGS__ ) -#endif - -// "BDD-style" convenience wrappers -#define CATCH_SCENARIO( ... ) INTERNAL_CATCH_TESTCASE_NO_REGISTRATION(INTERNAL_CATCH_UNIQUE_NAME( ____C_A_T_C_H____T_E_S_T____ )) -#define CATCH_SCENARIO_METHOD( className, ... ) INTERNAL_CATCH_TESTCASE_METHOD_NO_REGISTRATION(INTERNAL_CATCH_UNIQUE_NAME( ____C_A_T_C_H____T_E_S_T____ ), className ) -#define CATCH_GIVEN( desc ) -#define CATCH_AND_GIVEN( desc ) -#define CATCH_WHEN( desc ) -#define CATCH_AND_WHEN( desc ) -#define CATCH_THEN( desc ) -#define CATCH_AND_THEN( desc ) - -#define CATCH_STATIC_REQUIRE( ... ) (void)(0) -#define CATCH_STATIC_REQUIRE_FALSE( ... ) (void)(0) - -// If CATCH_CONFIG_PREFIX_ALL is not defined then the CATCH_ prefix is not required -#else - -#define REQUIRE( ... ) (void)(0) -#define REQUIRE_FALSE( ... ) (void)(0) - -#define REQUIRE_THROWS( ... ) (void)(0) -#define REQUIRE_THROWS_AS( expr, exceptionType ) (void)(0) -#define REQUIRE_THROWS_WITH( expr, matcher ) (void)(0) -#if !defined(CATCH_CONFIG_DISABLE_MATCHERS) -#define REQUIRE_THROWS_MATCHES( expr, exceptionType, matcher ) (void)(0) -#endif // CATCH_CONFIG_DISABLE_MATCHERS -#define REQUIRE_NOTHROW( ... ) (void)(0) - -#define CHECK( ... ) (void)(0) -#define CHECK_FALSE( ... ) (void)(0) -#define CHECKED_IF( ... ) if (__VA_ARGS__) -#define CHECKED_ELSE( ... ) if (!(__VA_ARGS__)) -#define CHECK_NOFAIL( ... ) (void)(0) - -#define CHECK_THROWS( ... ) (void)(0) -#define CHECK_THROWS_AS( expr, exceptionType ) (void)(0) -#define CHECK_THROWS_WITH( expr, matcher ) (void)(0) -#if !defined(CATCH_CONFIG_DISABLE_MATCHERS) -#define CHECK_THROWS_MATCHES( expr, exceptionType, matcher ) (void)(0) -#endif // CATCH_CONFIG_DISABLE_MATCHERS -#define CHECK_NOTHROW( ... ) (void)(0) - -#if !defined(CATCH_CONFIG_DISABLE_MATCHERS) -#define CHECK_THAT( arg, matcher ) (void)(0) - -#define REQUIRE_THAT( arg, matcher ) (void)(0) -#endif // CATCH_CONFIG_DISABLE_MATCHERS - -#define INFO( msg ) (void)(0) -#define UNSCOPED_INFO( msg ) (void)(0) -#define WARN( msg ) (void)(0) -#define CAPTURE( msg ) (void)(0) - -#define TEST_CASE( ... ) INTERNAL_CATCH_TESTCASE_NO_REGISTRATION(INTERNAL_CATCH_UNIQUE_NAME( ____C_A_T_C_H____T_E_S_T____ )) -#define TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_TESTCASE_NO_REGISTRATION(INTERNAL_CATCH_UNIQUE_NAME( ____C_A_T_C_H____T_E_S_T____ )) -#define METHOD_AS_TEST_CASE( method, ... ) -#define REGISTER_TEST_CASE( Function, ... ) (void)(0) -#define SECTION( ... ) -#define DYNAMIC_SECTION( ... ) -#define FAIL( ... ) (void)(0) -#define FAIL_CHECK( ... ) (void)(0) -#define SUCCEED( ... ) (void)(0) -#define ANON_TEST_CASE() INTERNAL_CATCH_TESTCASE_NO_REGISTRATION(INTERNAL_CATCH_UNIQUE_NAME( ____C_A_T_C_H____T_E_S_T____ )) - -#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR -#define TEMPLATE_TEST_CASE( ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE_NO_REGISTRATION(__VA_ARGS__) -#define TEMPLATE_TEST_CASE_SIG( ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE_SIG_NO_REGISTRATION(__VA_ARGS__) -#define TEMPLATE_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_NO_REGISTRATION(className, __VA_ARGS__) -#define TEMPLATE_TEST_CASE_METHOD_SIG( className, ... ) INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_SIG_NO_REGISTRATION(className, __VA_ARGS__ ) -#define TEMPLATE_PRODUCT_TEST_CASE( ... ) TEMPLATE_TEST_CASE( __VA_ARGS__ ) -#define TEMPLATE_PRODUCT_TEST_CASE_SIG( ... ) TEMPLATE_TEST_CASE( __VA_ARGS__ ) -#define TEMPLATE_PRODUCT_TEST_CASE_METHOD( className, ... ) TEMPLATE_TEST_CASE_METHOD( className, __VA_ARGS__ ) -#define TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG( className, ... ) TEMPLATE_TEST_CASE_METHOD( className, __VA_ARGS__ ) -#else -#define TEMPLATE_TEST_CASE( ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE_NO_REGISTRATION(__VA_ARGS__) ) -#define TEMPLATE_TEST_CASE_SIG( ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE_SIG_NO_REGISTRATION(__VA_ARGS__) ) -#define TEMPLATE_TEST_CASE_METHOD( className, ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_NO_REGISTRATION(className, __VA_ARGS__ ) ) -#define TEMPLATE_TEST_CASE_METHOD_SIG( className, ... ) INTERNAL_CATCH_EXPAND_VARGS( INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_SIG_NO_REGISTRATION(className, __VA_ARGS__ ) ) -#define TEMPLATE_PRODUCT_TEST_CASE( ... ) TEMPLATE_TEST_CASE( __VA_ARGS__ ) -#define TEMPLATE_PRODUCT_TEST_CASE_SIG( ... ) TEMPLATE_TEST_CASE( __VA_ARGS__ ) -#define TEMPLATE_PRODUCT_TEST_CASE_METHOD( className, ... ) TEMPLATE_TEST_CASE_METHOD( className, __VA_ARGS__ ) -#define TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG( className, ... ) TEMPLATE_TEST_CASE_METHOD( className, __VA_ARGS__ ) -#endif - -#define STATIC_REQUIRE( ... ) (void)(0) -#define STATIC_REQUIRE_FALSE( ... ) (void)(0) - -#endif - -#define CATCH_TRANSLATE_EXCEPTION( signature ) INTERNAL_CATCH_TRANSLATE_EXCEPTION_NO_REG( INTERNAL_CATCH_UNIQUE_NAME( catch_internal_ExceptionTranslator ), signature ) - -// "BDD-style" convenience wrappers -#define SCENARIO( ... ) INTERNAL_CATCH_TESTCASE_NO_REGISTRATION(INTERNAL_CATCH_UNIQUE_NAME( ____C_A_T_C_H____T_E_S_T____ ) ) -#define SCENARIO_METHOD( className, ... ) INTERNAL_CATCH_TESTCASE_METHOD_NO_REGISTRATION(INTERNAL_CATCH_UNIQUE_NAME( ____C_A_T_C_H____T_E_S_T____ ), className ) - -#define GIVEN( desc ) -#define AND_GIVEN( desc ) -#define WHEN( desc ) -#define AND_WHEN( desc ) -#define THEN( desc ) -#define AND_THEN( desc ) - -using Catch::Detail::Approx; - -#endif - -#endif // ! CATCH_CONFIG_IMPL_ONLY - -// start catch_reenable_warnings.h - - -#ifdef __clang__ -# ifdef __ICC // icpc defines the __clang__ macro -# pragma warning(pop) -# else -# pragma clang diagnostic pop -# endif -#elif defined __GNUC__ -# pragma GCC diagnostic pop -#endif - -// end catch_reenable_warnings.h -// end catch.hpp -#endif // TWOBLUECUBES_SINGLE_INCLUDE_CATCH_HPP_INCLUDED - diff --git a/res/library-schema.json b/res/library-schema.json index d9a7a97e..81ddc8d6 100644 --- a/res/library-schema.json +++ b/res/library-schema.json @@ -22,6 +22,14 @@ "pattern": "^[A-z][A-z0-9_]*((\\.|-)[A-z0-9_]+)*/[A-z][A-z0-9_]*((\\.|-)[A-z0-9_]+)*$" } }, + "test_uses": { + "type": "array", + "items": { + "type": "string", + "description": "A library that is used by this library for tests only. Should be of the form `namespace/name`.", + "pattern": "^[A-z][A-z0-9_]*((\\.|-)[A-z0-9_]+)*/[A-z][A-z0-9_]*((\\.|-)[A-z0-9_]+)*$" + } + }, "links": { "type": "array", "items": { @@ -31,4 +39,4 @@ } } } -} \ No newline at end of file +} diff --git a/res/package-schema.json b/res/package-schema.json index e081ccf1..0e565a9d 100644 --- a/res/package-schema.json +++ b/res/package-schema.json @@ -37,6 +37,12 @@ "type": "string" } }, + "test_depends": { + "type": "array", + "items": { + "type": "string" + } + }, "test_driver": { "type": "string", "default": "Catch-Main", @@ -46,4 +52,4 @@ ] } } -} \ No newline at end of file +} diff --git a/res/toolchain-schema.json b/res/toolchain-schema.json index 81f4b777..7f9eac0e 100644 --- a/res/toolchain-schema.json +++ b/res/toolchain-schema.json @@ -164,6 +164,18 @@ "description": "Set the base warning flags for the toolchain. These are always prepended to `warning_flags`.", "$ref": "#/definitions/command_line_flags" }, + "base_flags": { + "description": "Set the base compile flags for the toolchain. These are always prepended to `flags`.", + "$ref": "#/definitions/command_line_flags" + }, + "base_c_flags": { + "description": "Set the base C compile flags for the toolchain. These are always prepended to `flags`.", + "$ref": "#/definitions/command_line_flags" + }, + "base_cxx_flags": { + "description": "Set the base C++ compile flags for the toolchain. These are always prepended to `flags`.", + "$ref": "#/definitions/command_line_flags" + }, "c_compile_file": { "description": "Set the command template for compiling C source files", "$ref": "#/definitions/command_line_flags" @@ -211,4 +223,4 @@ } } } -} \ No newline at end of file +} diff --git a/site/index.html b/site/index.html deleted file mode 100644 index 00af3dec..00000000 --- a/site/index.html +++ /dev/null @@ -1,291 +0,0 @@ - - - - - - - DDS - - - - - -
-
DDS - The Simplest
- -
-
-
-
DDS
-
Tooling for a new decade
-
-
-
- -
What is DDS?
-
- DDS is a new build, test, and packaging tool for native C and C++ - libraries and applications, with a focus on simplicity, speed, and - integratability. -
- -
What makes DDS different?
-
- Convention over configuration. -

- Traditional build tools have been built to cede all control to their - users. While this may sound appealing, it leads to rampant ecosystem - fragmentation that impedes the ability for us to compose our - libraries and tools. -

- DDS's approach is to trade this flexibility for something sorely lacking - in the C and C++ build and distribution ecosystem: Simplicity. -
- -
Does DDS replace [XYZ] build tool?
-
- While DDS is a build tool, it is not meant as a full replacement - for all use cases of other build tools (e.g. CMake and Meson). -

- For many use cases, DDS will serve as a possible replacement for more - flexible build systems, especially when the build itself is far simpler - than is warranted by the flexibility offered by those tools. -

- Additionally, DDS is built to augment other build tools. The - output from DDS can be fed into other build and packaging systems. -
- -
Does DDS replace [XYZ] packaging tool?
-
- Yes and no. -

- DDS supports dependency resolution, procurement, and building, much like - many other tools, but has a few important differences of opinion. Refer - to the documentation for more information. -
- -
Is DDS "production ready"?
-
- At the time of writing, DDS is still in its alpha stages, is missing - several end-goal features, and will have several breaking changes before - its first "1.0" release. -

- Even then, DDS is ready to be used for experiments, hobby projects, and - in any place that doesn't require stability. -
- -
Is DDS free?
-
- Yes! DDS and its source code are available free of charge. -
- -
Is DDS open source?
-
- Yes! -

- The main DDS codebase is licensed under the - Mozilla Public License Version 2.0, although it is built upon - many components variously licensed as MIT, BSD, Boost, and public domain. -
- -
What platforms are supported?
-
- DDS is built, tested, distributed, and supported on Windows, macOS, - Linux, and FreeBSD. -

- The Microsoft Visual C++, GNU GCC, LLVM/Clang, and AppleClang compilers - are all supported. -
- -
Is DDS centralized?
-
- No / "not yet" -

- DDS's package procurement is in very early stages, and there is no - centralized repository of packages available for download. -

- DDS maintains a local catalog database that contain instructions it can - use to obtain packages from the internet. At the moment, this involves - cloning a Git repository, but there are plans to support catalog updates - and source distributions delivered over HTTP(S) in the future. This - includes the ability to host private package repositories. -
-
- - - - - \ No newline at end of file diff --git a/src/dds.main.cpp b/src/bpt.main.cpp similarity index 54% rename from src/dds.main.cpp rename to src/bpt.main.cpp index e28e9eb1..68b2f33a 100644 --- a/src/dds.main.cpp +++ b/src/bpt.main.cpp @@ -1,17 +1,21 @@ -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include #include #include -#include +#include #include #include #include +#include +#include +#include #include #include @@ -21,7 +25,7 @@ using namespace fansi::literals; static void load_locale() { - auto lang = dds::getenv("LANG"); + auto lang = bpt::getenv("LANG"); if (!lang) { return; } @@ -34,45 +38,42 @@ static void load_locale() { } int main_fn(std::string_view program_name, const std::vector& argv) { - dds::log::init_logger(); - auto log_subscr = neo::subscribe(&dds::log::ev_log::print); + bpt::log::init_logger(); + neo::listener log_listener = &bpt::log::ev_log::print; load_locale(); std::setlocale(LC_CTYPE, ".utf8"); - dds::install_signal_handlers(); - dds::enable_ansi_console(); + bpt::enable_ansi_console(); - dds::cli::options opts; + bpt::cli::options opts; debate::argument_parser parser; opts.setup_parser(parser); auto result = boost::leaf::try_catch( [&]() -> std::optional { parser.parse_argv(argv); - return {}; + return std::nullopt; }, [&](debate::help_request, debate::e_argument_parser p) { - std::cout << p.parser.help_string(program_name); + std::cout << p.value.help_string(program_name); return 0; }, [&](debate::unrecognized_argument, debate::e_argument_parser p, debate::e_arg_spelling arg, debate::e_did_you_mean* dym) { - std::cerr << p.parser.usage_string(program_name) << '\n'; - if (p.parser.subparsers()) { + std::cerr << p.value.usage_string(program_name) << '\n'; + if (p.value.subparsers()) { fmt::print(std::cerr, "Unrecognized argument/subcommand: \".bold.red[{}]\"\n"_styled, - arg.spelling); + arg.value); } else { fmt::print(std::cerr, "Unrecognized argument: \".bold.red[{}]\"\n"_styled, - arg.spelling); + arg.value); } if (dym) { - fmt::print(std::cerr, - " (Did you mean '.br.yellow[{}]'?)\n"_styled, - dym->candidate); + fmt::print(std::cerr, " (Did you mean '.br.yellow[{}]'?)\n"_styled, dym->value); } return 2; }, @@ -81,12 +82,12 @@ int main_fn(std::string_view program_name, const std::vector& argv) debate::e_argument_parser p, debate::e_arg_spelling spell, debate::e_invalid_arg_value val) { - std::cerr << p.parser.usage_string(program_name) << '\n'; + std::cerr << p.value.usage_string(program_name) << '\n'; fmt::print(std::cerr, - "Invalid {} value '{}' given for '{}'\n", - arg.argument.valname, - val.given, - spell.spelling); + "Invalid .cyan[{}] value \".bold.red[{}]\" given for '.yellow[{}]'\n"_styled, + arg.value.valname, + val.value, + spell.value); return 2; }, [&](debate::invalid_arguments, @@ -94,43 +95,45 @@ int main_fn(std::string_view program_name, const std::vector& argv) debate::e_arg_spelling spell, debate::e_argument arg, debate::e_wrong_val_num given) { - std::cerr << p.parser.usage_string(program_name) << '\n'; - if (arg.argument.nargs == 0) { - fmt::print(std::cerr, - "Argument '{}' does not expect any values, but was given one\n", - spell.spelling); - } else if (arg.argument.nargs == 1 && given.n_given == 0) { - fmt::print(std::cerr, - "Argument '{}' expected to be given a value, but received none\n", - spell.spelling); + std::cerr << p.value.usage_string(program_name) << '\n'; + if (arg.value.nargs == 0) { + fmt::print( + std::cerr, + "Argument .yellow[{}] does not expect any values, but was given one\n"_styled, + spell.value); + } else if (arg.value.nargs == 1 && given.value == 0) { + fmt::print( + std::cerr, + "Argument .yellow[{}] expected to be given a value, but received none\n"_styled, + spell.value); } else { fmt::print( std::cerr, - "Wrong number of arguments provided for '{}': Expected {}, but only got {}\n", - spell.spelling, - arg.argument.nargs, - given.n_given); + "Wrong number of arguments provided for .yellow[{}]: Expected {}, but only got {}\n"_styled, + spell.value, + arg.value.nargs, + given.value); } return 2; }, [&](debate::missing_required, debate::e_argument_parser p, debate::e_argument arg) { fmt::print(std::cerr, - "{}\nMissing required argument '{}'\n", - p.parser.usage_string(program_name), - arg.argument.preferred_spelling()); + "{}\nMissing required argument '.yellow[{}]'\n"_styled, + p.value.usage_string(program_name), + arg.value.preferred_spelling()); return 2; }, - [&](debate::invalid_repitition, debate::e_argument_parser p, debate::e_arg_spelling sp) { + [&](debate::invalid_repetition, debate::e_argument_parser p, debate::e_arg_spelling sp) { fmt::print(std::cerr, - "{}\nArgument '{}' cannot be provided more than once\n", - p.parser.usage_string(program_name), - sp.spelling); + "{}\nArgument '.yellow[{}]' cannot be provided more than once\n"_styled, + p.value.usage_string(program_name), + sp.value); return 2; }, - [&](debate::missing_required err, debate::e_argument_parser p) { + [&](debate::invalid_arguments const& err, debate::e_argument_parser p) { fmt::print(std::cerr, "{}\nError: {}\n", - p.parser.usage_string(program_name), + p.value.usage_string(program_name), err.what()); return 2; }); @@ -138,8 +141,28 @@ int main_fn(std::string_view program_name, const std::vector& argv) // Non-null result from argument parsing, return that value immediately. return *result; } - dds::log::current_log_level = opts.log_level; - return dds::cli::dispatch_main(opts); + if (opts.subcommand != bpt::cli::subcommand::new_) { + // We want ^C to behave as-normal for 'new' + bpt::install_signal_handlers(); + } + bpt::log::current_log_level = opts.log_level; + neo::opt_listener log_sqlite3 = [&](neo::sqlite3::event::step ev) { + auto msg = neo::sqlite3::error_category().message(static_cast(ev.ec)); + bpt_log(trace, "SQLite step: .bold.white[{}]"_styled, ev.st.expanded_sql_string()); + if (neo::sqlite3::is_error_rc(ev.ec)) { + bpt_log(trace, + " Error: .bold.red[{}]: .bold.yellow[{}]"_styled, + msg, + ev.st.connection().error_message()); + } else { + bpt_log(trace, " Okay: .bold.green[{}]"_styled, msg); + } + }; + + if (opts.log_level >= bpt::log::level::trace and bpt::config::enable_sqlite3_trace()) { + log_sqlite3.start_listening(); + } + return bpt::cli::dispatch_main(opts); } #if NEO_OS_IS_WINDOWS diff --git a/src/bpt/bpt.test.hpp b/src/bpt/bpt.test.hpp new file mode 100644 index 00000000..6fee4b6b --- /dev/null +++ b/src/bpt/bpt.test.hpp @@ -0,0 +1,37 @@ +#include + +#if !__bpt_header_check +#include + +#include + +namespace bpt::testing { + +const auto REPO_ROOT = fs::canonical((fs::path(__FILE__) / "../../..").lexically_normal()); +const auto DATA_DIR = REPO_ROOT / "data"; + +template +constexpr auto leaf_handle_nofail(Fn&& fn) { + using rtype = decltype(fn()); + if constexpr (boost::leaf::is_result_type::value) { + return boost::leaf::try_handle_all( // + fn, + [](const boost::leaf::verbose_diagnostic_info& info) -> decltype(fn().value()) { + FAIL("Operation failed: " << info); + std::terminate(); + }); + } else { + return boost::leaf::try_catch( // + fn, + [](const boost::leaf::verbose_diagnostic_info& info) -> decltype(fn()) { + FAIL("Operation failed: " << info); + std::terminate(); + }); + } +} +#define REQUIRES_LEAF_NOFAIL(...) \ + (::bpt::testing::leaf_handle_nofail([&] { return (__VA_ARGS__); })) + +} // namespace bpt::testing + +#endif \ No newline at end of file diff --git a/src/bpt/build/builder.cpp b/src/bpt/build/builder.cpp new file mode 100644 index 00000000..f06cd0f8 --- /dev/null +++ b/src/bpt/build/builder.cpp @@ -0,0 +1,388 @@ +#include "./builder.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace bpt; +using namespace fansi::literals; + +using json = nlohmann::ordered_json; + +namespace { + +void log_failure(const test_failure& fail) { + bpt_log(error, + "Test .br.yellow[{}] .br.red[{}] [Exited {}]"_styled, + fail.executable_path.string(), + fail.timed_out ? "TIMED OUT" : "FAILED", + fail.retc); + if (fail.signal) { + bpt_log(error, "Test execution received signal {}", fail.signal); + } + if (trim_view(fail.output).empty()) { + bpt_log(error, "(Test executable produced no output)"); + } else { + bpt_log(error, "Test output:\n{}[bpt - test output end]", fail.output); + } +} + +///? XXX: This library activating method is a poor hack to ensure only libraries that are used by +///? the main libraries are actually built. +/// TODO: Make this less ugly. +struct lib_prep_info { + const sdist_target& sdt; + const crs::library_info& lib; + const crs::package_info& pkg; +}; + +library_plan prepare_library(const sdist_target& sdt, + const crs::library_info& lib, + const crs::package_info& pkg_man) { + library_build_params lp; + lp.out_subdir = normalize_path(sdt.params.subdir / lib.path); + lp.build_apps = sdt.params.build_apps; + lp.build_tests = sdt.params.build_tests; + lp.enable_warnings = sdt.params.enable_warnings; + return library_plan::create(sdt.sd.path, pkg_man, lib, std::move(lp)); +} + +static void activate_more(neo::output> libs_to_build, + std::map const& all_libs, + const lib_prep_info& inf) { + auto did_insert = libs_to_build.get().emplace(&inf).second; + if (not did_insert) { + return; + } + auto add_dep = [&](const crs::dependency& dep) { + for (bpt::name const& used_name : dep.uses) { + auto found = all_libs.find(lm::usage{dep.name.str, used_name.str}); + neo_assert(invariant, + found != all_libs.end(), + "Failed to find a dependency library for build activation", + inf.lib.name, + dep.name, + used_name); + activate_more(libs_to_build, all_libs, found->second); + } + }; + auto add_siblings = [&](const name& sibling_name) { + auto sibling = all_libs.find(lm::usage{inf.pkg.id.name.str, sibling_name.str}); + neo_assert(invariant, + sibling != all_libs.end(), + "Failed to find sibling library for 'using'"); + activate_more(libs_to_build, all_libs, sibling->second); + }; + for (crs::dependency const& dep : inf.lib.dependencies) { + add_dep(dep); + } + for (name const& use : inf.lib.intra_using) { + add_siblings(use); + } + if (inf.sdt.params.build_tests) { + for (crs::dependency const& dep : inf.lib.test_dependencies) { + add_dep(dep); + } + for (const name& use : inf.lib.intra_test_using) { + add_siblings(use); + } + } +} + +build_plan prepare_build_plan(neo::ranges::range_of auto&& sdists) { + build_plan plan; + // First generate a mapping of all libraries + std::map all_libs; + // Keep track of which libraries we want to build: + std::set libs_to_build; + std::set leaf_libs_to_build; + // Iterate all loaded sdists: + for (const sdist_target& sdt : sdists) { + for (const auto& lib : sdt.sd.pkg.libraries) { + const lib_prep_info& lpi = // + all_libs + .emplace(lm::usage(sdt.sd.pkg.id.name.str, lib.name.str), + lib_prep_info{ + .sdt = sdt, + .lib = lib, + .pkg = sdt.sd.pkg, + }) + .first->second; + // If the sdist_build_params wants libraries, activate them now: + if (ranges::contains(sdt.params.build_libraries, lib.name)) { + leaf_libs_to_build.emplace(&lpi); + } + } + } + for (auto lib : leaf_libs_to_build) { + activate_more(neo::into(libs_to_build), all_libs, *lib); + } + // Create package plans for each library and the package it owns: + std::map pkg_plans; + for (auto lp : libs_to_build) { + package_plan pkg{lp->sdt.sd.pkg.id.name.str}; + auto cur = pkg_plans.find(lp->sdt.sd.pkg.id.name); + if (cur == pkg_plans.end()) { + cur = pkg_plans + .emplace(lp->sdt.sd.pkg.id.name, package_plan{lp->sdt.sd.pkg.id.name.str}) + .first; + } + cur->second.add_library(prepare_library(lp->sdt, lp->lib, lp->pkg)); + } + // Add all the packages to the plan: + for (const auto& pair : pkg_plans) { + plan.add_package(std::move(pair.second)); + } + return plan; +} + +usage_requirements +prepare_ureqs(const build_plan& plan, const toolchain& toolchain, path_ref out_root) { + usage_requirement_map ureqs; + for (const auto& pkg : plan.packages()) { + for (const auto& lib : pkg.libraries()) { + auto& lib_reqs = ureqs.add({pkg.name(), std::string(lib.name())}); + lib_reqs.include_paths.push_back(lib.public_include_dir()); + lib_reqs.uses = lib.lib_uses(); + //! lib_reqs.links = lib.library_().manifest().links; + if (const auto& arc = lib.archive_plan()) { + lib_reqs.linkable_path = out_root / arc->calc_archive_file_path(toolchain); + } + } + } + return usage_requirements(std::move(ureqs)); +} + +json get_built_lib(build_env_ref env, const library_plan& lib) { + auto ret = json::object(); + ret.emplace("name", std::string(lib.name())); + if (auto const& ar = lib.archive_plan(); ar.has_value()) { + ret.emplace("path", + normalize_path(env.output_root / ar->calc_archive_file_path(env.toolchain)) + .generic_string()); + } + ret.emplace("include-path", normalize_path(lib.public_include_dir()).generic_string()); + auto uses = json::array(); + for (auto&& use : lib.lib_uses()) { + uses.push_back(json::object({ + {"package", use.namespace_}, + {"library", use.name}, + })); + } + ret.emplace("uses", std::move(uses)); + return ret; +} + +json get_built_pkg(build_env_ref env, const package_plan& pkg) { + auto ret = json::object(); + auto libs = json::array(); + + for (const auto& lib : pkg.libraries()) { + auto l = get_built_lib(env, lib); + libs.push_back(std::move(l)); + } + + ret.emplace("libraries", std::move(libs)); + return ret; +} + +void write_built_json(build_env_ref env, const build_plan& plan, path_ref json_path) { + auto root = json::object({{"version", 1}}); + auto pkgs = json::object(); + fs::create_directories(fs::absolute(json_path).parent_path()); + for (const auto& pkg : plan.packages()) { + auto p = get_built_pkg(env, pkg); + pkgs.emplace(pkg.name(), std::move(p)); + } + root.emplace("packages", std::move(pkgs)); + bpt::write_file(json_path, root.dump(2)); +} + +void write_lib_cmake(build_env_ref env, + std::ostream& out, + const package_plan& /* pkg */, + const library_plan& lib) { + fmt::print(out, "# Library {}\n", lib.qualified_name()); + auto cmake_name = fmt::format("{}", bpt::replace(lib.qualified_name(), "/", "::")); + auto cm_kind = lib.archive_plan().has_value() ? "STATIC" : "INTERFACE"; + fmt::print( + out, + "if(TARGET {0})\n" + " get_target_property(bpt_imported {0} bpt_IMPORTED)\n" + " if(NOT bpt_imported)\n" + " message(WARNING [[A target \"{0}\" is already defined, and not by a bpt import]])\n" + " endif()\n" + "else()\n", + cmake_name); + fmt::print(out, + " add_library({0} {1} IMPORTED GLOBAL)\n" + " set_property(TARGET {0} PROPERTY bpt_IMPORTED TRUE)\n" + " set_property(TARGET {0} PROPERTY INTERFACE_INCLUDE_DIRECTORIES [[{2}]])\n", + cmake_name, + cm_kind, + lib.public_include_dir().generic_string()); + for (auto&& use : lib.lib_uses()) { + fmt::print(out, + " set_property(TARGET {} APPEND PROPERTY INTERFACE_LINK_LIBRARIES {}::{})\n", + cmake_name, + use.namespace_, + use.name); + } + //! for (auto&& link : lib.links()) { + //! fmt::print(out, + //! " set_property(TARGET {} APPEND PROPERTY\n" + //! " INTERFACE_LINK_LIBRARIES $)\n", + //! cmake_name, + //! link.namespace_, + //! link.name); + //! } + if (auto& arc = lib.archive_plan()) { + fmt::print(out, + " set_property(TARGET {} PROPERTY IMPORTED_LOCATION [[{}]])\n", + cmake_name, + (env.output_root / arc->calc_archive_file_path(env.toolchain)).generic_string()); + } + fmt::print(out, "endif()\n"); +} + +void write_cmake_pkg(build_env_ref env, std::ostream& out, const package_plan& pkg) { + fmt::print(out, "## Imports for {}\n", pkg.name()); + for (auto& lib : pkg.libraries()) { + write_lib_cmake(env, out, pkg, lib); + } + fmt::print(out, "\n"); +} + +void write_cmake(build_env_ref env, const build_plan& plan, path_ref cmake_out) { + fs::create_directories(fs::absolute(cmake_out).parent_path()); + auto out = bpt::open_file(cmake_out, std::ios::binary | std::ios::out); + out << "## This CMake file was generated by `bpt build-deps`. DO NOT EDIT!\n\n"; + for (const auto& pkg : plan.packages()) { + write_cmake_pkg(env, out, pkg); + } +} + +/** + * @brief Calculate a hash of the directory layout of the given directory. + * + * Because a tweaks-dir is specifically designed to have files added/removed within it, and + * its contents are inspected by `__has_include`, we need to have a way to invalidate any caches + * when the content of that directory changes. We don't care to hash the contents of the files, + * since those will already break any caches. + */ +std::string hash_tweaks_dir(const fs::path& tweaks_dir) { + if (!fs::is_directory(tweaks_dir)) { + return "0"; // No tweaks directory, no cache to bust + } + std::vector children{fs::recursive_directory_iterator{tweaks_dir}, + fs::recursive_directory_iterator{}}; + std::sort(children.begin(), children.end()); + // A really simple inline djb2 hash + std::uint32_t hash = 5381; + for (auto& p : children) { + for (std::uint32_t c : fs::weakly_canonical(p).string()) { + hash = ((hash << 5) + hash) + c; + } + } + return std::to_string(hash); +} + +template +void with_build_plan(const build_params& params, + const std::vector& sdists, + Func&& fn) { + fs::create_directories(params.out_root); + auto db = database::open(params.out_root / ".bpt.db"); + + auto plan = prepare_build_plan(sdists); + auto ureqs = prepare_ureqs(plan, params.toolchain, params.out_root); + build_env env{ + params.toolchain, + params.out_root, + db, + toolchain_knobs{ + .is_tty = stdout_is_a_tty(), + .tweaks_dir = params.tweaks_dir, + }, + ureqs, + }; + + if (env.knobs.tweaks_dir) { + env.knobs.cache_buster = hash_tweaks_dir(*env.knobs.tweaks_dir); + bpt_log(trace, + "Build cache-buster value for tweaks-dir [{}] content is '{}'", + *env.knobs.tweaks_dir, + *env.knobs.cache_buster); + } + + if (params.generate_compdb) { + generate_compdb(plan, env); + } + + fn(std::move(env), std::move(plan)); +} + +} // namespace + +void builder::compile_files(const std::vector& files, const build_params& params) const { + with_build_plan(params, _sdists, [&](build_env_ref env, build_plan plan) { + plan.compile_files(env, params.parallel_jobs, files); + }); +} + +void builder::build(const build_params& params) const { + with_build_plan(params, _sdists, [&](build_env_ref env, const build_plan& plan) { + bpt::stopwatch sw; + plan.compile_all(env, params.parallel_jobs); + bpt_log(info, "Compilation completed in {:L}ms", sw.elapsed_ms().count()); + + sw.reset(); + plan.archive_all(env, params.parallel_jobs); + bpt_log(info, "Archiving completed in {:L}ms", sw.elapsed_ms().count()); + + sw.reset(); + plan.link_all(env, params.parallel_jobs); + bpt_log(info, "Runtime binary linking completed in {:L}ms", sw.elapsed_ms().count()); + + sw.reset(); + auto test_failures = plan.run_all_tests(env, params.parallel_jobs); + bpt_log(info, "Test execution finished in {:L}ms", sw.elapsed_ms().count()); + + for (auto& fail : test_failures) { + log_failure(fail); + } + if (!test_failures.empty()) { + BOOST_LEAF_THROW_EXCEPTION(make_user_error(), + test_failures, + BPT_ERR_REF("test-failure")); + } + + if (params.emit_built_json) { + write_built_json(env, plan, *params.emit_built_json); + } + + if (params.emit_cmake) { + write_cmake(env, plan, *params.emit_cmake); + } + }); +} diff --git a/src/dds/build/builder.hpp b/src/bpt/build/builder.hpp similarity index 86% rename from src/dds/build/builder.hpp rename to src/bpt/build/builder.hpp index e8483a69..469f22ee 100644 --- a/src/dds/build/builder.hpp +++ b/src/bpt/build/builder.hpp @@ -1,12 +1,13 @@ #pragma once -#include -#include +#include +#include #include #include +#include -namespace dds { +namespace bpt { /** * Parameters for building an individual source distribution as part of a larger build plan. @@ -22,6 +23,8 @@ struct sdist_build_params { bool build_apps = false; /// Whether to enable build warnings bool enable_warnings = false; + /// The libraries in this source distribution that we must build + std::vector build_libraries; }; /** @@ -60,4 +63,4 @@ class builder { void compile_files(const std::vector& files, const build_params& params) const; }; -} // namespace dds +} // namespace bpt diff --git a/src/dds/build/file_deps.cpp b/src/bpt/build/file_deps.cpp similarity index 61% rename from src/dds/build/file_deps.cpp rename to src/bpt/build/file_deps.cpp index 37548ea5..a679530b 100644 --- a/src/dds/build/file_deps.cpp +++ b/src/bpt/build/file_deps.cpp @@ -1,23 +1,21 @@ #include "./file_deps.hpp" -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include -#include -#include -#include +#include -using namespace dds; +using namespace bpt; -file_deps_info dds::parse_mkfile_deps_file(path_ref where) { - auto content = slurp_file(where); +file_deps_info bpt::parse_mkfile_deps_file(path_ref where) { + auto content = bpt::read_file(where); return parse_mkfile_deps_str(content); } -file_deps_info dds::parse_mkfile_deps_str(std::string_view str) { +file_deps_info bpt::parse_mkfile_deps_str(std::string_view str) { file_deps_info ret; // Remove escaped newlines @@ -27,14 +25,14 @@ file_deps_info dds::parse_mkfile_deps_str(std::string_view str) { auto iter = split.begin(); auto stop = split.end(); if (iter == stop) { - dds_log(critical, + bpt_log(critical, "Invalid deps listing. Shell split was empty. This is almost certainly a bug."); return ret; } auto& head = *iter; ++iter; if (!ends_with(head, ":")) { - dds_log( + bpt_log( critical, "Invalid deps listing. Leader item is not colon-terminated. This is probably a bug. " "(Are you trying to use C++ Modules? That's not ready yet, sorry. Set `Deps-Mode` to " @@ -46,7 +44,7 @@ file_deps_info dds::parse_mkfile_deps_str(std::string_view str) { return ret; } -msvc_deps_info dds::parse_msvc_output_for_deps(std::string_view output, std::string_view leader) { +msvc_deps_info bpt::parse_msvc_output_for_deps(std::string_view output, std::string_view leader) { auto lines = split_view(output, "\n"); std::string cleaned_output; file_deps_info deps; @@ -67,17 +65,17 @@ msvc_deps_info dds::parse_msvc_output_for_deps(std::string_view output, std::str return {deps, cleaned_output}; } -void dds::update_deps_info(neo::output db_, const file_deps_info& deps) { +void bpt::update_deps_info(neo::output db_, const file_deps_info& deps) { database& db = db_; db.record_compilation(deps.output, deps.command); db.forget_inputs_of(deps.output); for (auto&& inp : deps.inputs) { auto mtime = fs::last_write_time(inp); - db.record_dep(inp, deps.output, mtime); + db.record_dep(inp, deps.output, (std::min)(mtime, deps.compile_start_time)); } } -std::optional dds::get_prior_compilation(const database& db, +std::optional bpt::get_prior_compilation(const database& db, path_ref output_path) { auto cmd_ = db.command_of(output_path); if (!cmd_) { @@ -91,11 +89,25 @@ std::optional dds::get_prior_compilation(const database& db, auto& inputs = *inputs_; auto changed_files = // inputs // - | ranges::views::filter([](const input_file_info& input) { - return !fs::exists(input.path) || fs::last_write_time(input.path) != input.last_mtime; + | std::views::filter([](const input_file_info& input) { + if (input.path.extension() == ".syncheck") { + // Do not consider .syncheck files, as they will always be re-written and have no + // interesting content + return false; + } + if (!fs::exists(input.path)) { + // The input does not exist, so consider it out-of-date + return true; + } + if (fs::last_write_time(input.path) != input.prev_mtime) { + // The input has been modified since our last execution + return true; + } + // No "new" inputs + return false; }) - | ranges::views::transform([](auto& info) { return info.path; }) // - | ranges::to_vector; + | std::views::transform([](auto& info) { return info.path; }) // + | neo::to_vector; prior_compilation ret; ret.newer_inputs = std::move(changed_files); ret.previous_command = cmd; diff --git a/src/dds/build/file_deps.hpp b/src/bpt/build/file_deps.hpp similarity index 96% rename from src/dds/build/file_deps.hpp rename to src/bpt/build/file_deps.hpp index cefb9497..85384e87 100644 --- a/src/dds/build/file_deps.hpp +++ b/src/bpt/build/file_deps.hpp @@ -27,8 +27,8 @@ * other languages is not difficult. */ -#include -#include +#include +#include #include @@ -36,7 +36,7 @@ #include #include -namespace dds { +namespace bpt { /** * The mode in which we can scan for compilation dependencies. @@ -66,6 +66,10 @@ struct file_deps_info { * The command that was used to generate the output */ completed_compilation command; + /** + * The time at which compilation started. + */ + fs::file_time_type compile_start_time; }; class database; @@ -137,4 +141,4 @@ struct prior_compilation { */ std::optional get_prior_compilation(const database& db, path_ref output_path); -} // namespace dds +} // namespace bpt diff --git a/src/dds/build/file_deps.test.cpp b/src/bpt/build/file_deps.test.cpp similarity index 78% rename from src/dds/build/file_deps.test.cpp rename to src/bpt/build/file_deps.test.cpp index 8d40b3a0..16af9e4f 100644 --- a/src/dds/build/file_deps.test.cpp +++ b/src/bpt/build/file_deps.test.cpp @@ -1,19 +1,19 @@ -#include +#include #include -auto path_vec = [](auto... args) { return std::vector{args...}; }; +auto path_vec = [](auto... args) { return std::vector{args...}; }; TEST_CASE("Parse Makefile deps") { - auto deps = dds::parse_mkfile_deps_str("foo.o: bar.c"); + auto deps = bpt::parse_mkfile_deps_str("foo.o: bar.c"); CHECK(deps.output == "foo.o"); CHECK(deps.inputs == path_vec("bar.c")); // Newline is okay - deps = dds::parse_mkfile_deps_str("foo.o: bar.c \\\n baz.c"); + deps = bpt::parse_mkfile_deps_str("foo.o: bar.c \\\n baz.c"); CHECK(deps.output == "foo.o"); CHECK(deps.inputs == path_vec("bar.c", "baz.c")); - deps = dds::parse_mkfile_deps_str( + deps = bpt::parse_mkfile_deps_str( "/some-path/Ю́рий\\ Алексе́евич\\ Гага́рин/build/obj/foo.main.cpp.o: \\\n" " /foo.main.cpp \\\n" " /stdc-predef.h\n"); @@ -24,10 +24,10 @@ TEST_CASE("Parse Makefile deps") { TEST_CASE("Invalid deps") { // Invalid deps does not terminate. This will generate an error message in // the logs, but it is a non-fatal error that we can recover from. - auto deps = dds::parse_mkfile_deps_str("foo.o : cat"); + auto deps = bpt::parse_mkfile_deps_str("foo.o : cat"); CHECK(deps.output.empty()); CHECK(deps.inputs.empty()); - deps = dds::parse_mkfile_deps_str("foo.c"); + deps = bpt::parse_mkfile_deps_str("foo.c"); CHECK(deps.output.empty()); CHECK(deps.inputs.empty()); } @@ -43,12 +43,12 @@ Other line Something else )"; - auto res = dds::parse_msvc_output_for_deps(mscv_output, "Note: including file:"); + auto res = bpt::parse_msvc_output_for_deps(mscv_output, "Note: including file:"); auto& deps = res.deps_info; auto new_output = res.cleaned_output; CHECK(new_output == "\nOther line\n indented line\nSomething else\n"); CHECK(deps.inputs - == std::vector({ + == std::vector({ "C:\\foo\\bar\\filepath/thing.hpp", "C:\\foo\\bar\\filepath/baz.h", "C:\\foo\\bar\\filepath/quux.h", diff --git a/src/dds/build/iter_compilations.hpp b/src/bpt/build/iter_compilations.hpp similarity index 81% rename from src/dds/build/iter_compilations.hpp rename to src/bpt/build/iter_compilations.hpp index 224f1fab..8b1f8ffc 100644 --- a/src/dds/build/iter_compilations.hpp +++ b/src/bpt/build/iter_compilations.hpp @@ -1,13 +1,13 @@ #pragma once -#include +#include #include #include #include #include -namespace dds { +namespace bpt { /** * Iterate over every library defined as part of the build plan @@ -34,6 +34,11 @@ inline auto iter_compilations(const build_plan& plan) { | ranges::views::join // ; + auto header_compiles = // + iter_libraries(plan) // + | ranges::views::transform(&library_plan::headers) // + | ranges::views::join; + auto exe_compiles = // iter_libraries(plan) // | ranges::views::transform(&library_plan::executables) // @@ -41,7 +46,7 @@ inline auto iter_compilations(const build_plan& plan) { | ranges::views::transform(&link_executable_plan::main_compile_file) // ; - return ranges::views::concat(lib_compiles, exe_compiles); + return ranges::views::concat(lib_compiles, header_compiles, exe_compiles); } -} // namespace dds \ No newline at end of file +} // namespace bpt \ No newline at end of file diff --git a/src/dds/build/params.hpp b/src/bpt/build/params.hpp similarity index 52% rename from src/dds/build/params.hpp rename to src/bpt/build/params.hpp index 15856ef1..3d415f90 100644 --- a/src/dds/build/params.hpp +++ b/src/bpt/build/params.hpp @@ -1,22 +1,21 @@ #pragma once -#include -#include -#include +#include +#include +#include #include -namespace dds { +namespace bpt { struct build_params { fs::path out_root; - std::optional existing_lm_index; - std::optional emit_lmi; + std::optional emit_built_json; std::optional emit_cmake{}; std::optional tweaks_dir{}; - dds::toolchain toolchain; + bpt::toolchain toolchain; bool generate_compdb = true; int parallel_jobs = 0; }; -} // namespace dds +} // namespace bpt diff --git a/src/dds/build/plan/archive.cpp b/src/bpt/build/plan/archive.cpp similarity index 72% rename from src/dds/build/plan/archive.cpp rename to src/bpt/build/plan/archive.cpp index 4cdd2ebf..e1026ef8 100644 --- a/src/dds/build/plan/archive.cpp +++ b/src/bpt/build/plan/archive.cpp @@ -1,15 +1,17 @@ #include "./archive.hpp" -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include #include #include #include -using namespace dds; +using namespace bpt; using namespace fansi::literals; fs::path create_archive_plan::calc_archive_file_path(const toolchain& tc) const noexcept { @@ -38,7 +40,7 @@ void create_archive_plan::archive(const build_env& env) const { // Different archiving tools behave differently between platforms depending on whether the // archive file exists. Make it uniform by simply removing the prior copy. if (fs::exists(ar.out_path)) { - dds_log(debug, "Remove prior archive file [{}]", ar.out_path.string()); + bpt_log(debug, "Remove prior archive file [{}]", ar.out_path.string()); fs::remove(ar.out_path); } @@ -46,24 +48,25 @@ void create_archive_plan::archive(const build_env& env) const { fs::create_directories(ar.out_path.parent_path()); // Do it! - dds_log(info, "[{}] Archive: {}", _qual_name, out_relpath); + bpt_log(info, "[{}] Archive: {}", _qual_name, out_relpath); auto&& [dur_ms, ar_res] = timed( [&] { return run_proc(proc_options{.command = ar_cmd, .cwd = ar_cwd}); }); - dds_log(info, "[{}] Archive: {} - {:L}ms", _qual_name, out_relpath, dur_ms.count()); + bpt_log(info, "[{}] Archive: {} - {:L}ms", _qual_name, out_relpath, dur_ms.count()); // Check, log, and throw if (!ar_res.okay()) { - dds_log(error, + bpt_log(error, "Creating static library archive [{}] failed for '{}'", out_relpath, _qual_name); - dds_log(error, + bpt_log(error, "Subcommand FAILED: .bold.yellow[{}]\n{}"_styled, quote_command(ar_cmd), ar_res.output); - throw_external_error< - errc::archive_failure>("Creating static library archive [{}] failed for '{}'", - out_relpath, - _qual_name); + BOOST_LEAF_THROW_EXCEPTION(make_external_error( + "Creating static library archive [{}] failed for '{}'", + out_relpath, + _qual_name), + BPT_ERR_REF("archive-failure")); } } diff --git a/src/dds/build/plan/archive.hpp b/src/bpt/build/plan/archive.hpp similarity index 95% rename from src/dds/build/plan/archive.hpp rename to src/bpt/build/plan/archive.hpp index fbdaf8e3..cab15d30 100644 --- a/src/dds/build/plan/archive.hpp +++ b/src/bpt/build/plan/archive.hpp @@ -1,12 +1,12 @@ #pragma once -#include -#include +#include +#include #include #include -namespace dds { +namespace bpt { /** * Represents the intention to create an library archive. This also contains @@ -68,4 +68,4 @@ class create_archive_plan { void archive(build_env_ref env) const; }; -} // namespace dds +} // namespace bpt diff --git a/src/bpt/build/plan/base.hpp b/src/bpt/build/plan/base.hpp new file mode 100644 index 00000000..8afe565a --- /dev/null +++ b/src/bpt/build/plan/base.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +#include + +namespace bpt { + +struct build_env { + bpt::toolchain toolchain; + std::filesystem::path output_root; + database& db; + + toolchain_knobs knobs; + + const usage_requirements& ureqs; +}; + +using build_env_ref = const build_env&; + +} // namespace bpt diff --git a/src/dds/build/plan/compile_exec.cpp b/src/bpt/build/plan/compile_exec.cpp similarity index 80% rename from src/dds/build/plan/compile_exec.cpp rename to src/bpt/build/plan/compile_exec.cpp index 557fb0c6..d94446ed 100644 --- a/src/dds/build/plan/compile_exec.cpp +++ b/src/bpt/build/plan/compile_exec.cpp @@ -1,13 +1,14 @@ #include "./compile_exec.hpp" -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include @@ -20,7 +21,7 @@ #include #include -using namespace dds; +using namespace bpt; using namespace ranges; using namespace fansi::literals; @@ -41,6 +42,8 @@ struct compile_ticket { bool needs_recompile; // Information about the previous time a file was compiled, if any std::optional prior_command; + // Whether this compilation is for the purpose of header independence + bool is_syntax_only = false; }; /** @@ -61,7 +64,7 @@ handle_compilation(const compile_ticket& compile, build_env_ref env, compile_cou compile.plan.get().source_path(), quote_command(compile.command.command)); auto& prior = *compile.prior_command; - if (dds::trim_view(prior.output).empty()) { + if (bpt::trim_view(prior.output).empty()) { // Nothing to show return {}; } @@ -70,12 +73,12 @@ handle_compilation(const compile_ticket& compile, build_env_ref env, compile_cou // this block will be hit when the source file belongs to an external dependency. Rather // than continually spam the user with warnings that belong to dependencies, don't // repeatedly show them. - dds_log(trace, + bpt_log(trace, "Cached compiler output suppressed for file with disabled warnings ({})", compile.plan.get().source_path().string()); return {}; } - dds_log( + bpt_log( warn, "While compiling file .bold.cyan[{}] [.bold.yellow[{}]] (.br.blue[cached compiler output]):\n{}"_styled, compile.plan.get().source_path().string(), @@ -90,17 +93,20 @@ handle_compilation(const compile_ticket& compile, build_env_ref env, compile_cou // Generate a log message to display to the user auto source_path = compile.plan.get().source_path(); - auto msg - = fmt::format("[{}] Compile: .br.cyan[{}]"_styled, + std::string_view compile_event_msg = compile.is_syntax_only ? "Check" : "Compile"; + auto msg + = fmt::format("[{}] {}: .br.cyan[{}]"_styled, compile.plan.get().qualifier(), + compile_event_msg, fs::relative(source_path, compile.plan.get().source().basis_path).string()); // Do it! - dds_log(info, msg); + bpt_log(info, msg); + auto start_time = fs::file_time_type::clock::now(); auto&& [dur_ms, proc_res] = timed([&] { return run_proc(compile.command.command); }); auto nth = counter.n.fetch_add(1); - dds_log(info, + bpt_log(info, "{:60} - {:>7L}ms [{:{}}/{}]", msg, dur_ms.count(), @@ -125,13 +131,13 @@ handle_compilation(const compile_ticket& compile, build_env_ref env, compile_cou assert(compile.command.gnu_depfile_path.has_value()); auto& df_path = *compile.command.gnu_depfile_path; if (!fs::is_regular_file(df_path)) { - dds_log(critical, + bpt_log(critical, "The expected Makefile deps were not generated on disk. This is a bug! " "(Expected file to exist: [{}])", df_path.string()); } else { - dds_log(trace, "Loading compilation dependencies from {}", df_path.string()); - auto dep_info = dds::parse_mkfile_deps_file(df_path); + bpt_log(trace, "Loading compilation dependencies from {}", df_path.string()); + auto dep_info = bpt::parse_mkfile_deps_file(df_path); neo_assert(invariant, dep_info.output == compile.object_file_path, "Generated mkfile deps output path does not match the object file path that " @@ -145,7 +151,7 @@ handle_compilation(const compile_ticket& compile, build_env_ref env, compile_cou } } else if (env.toolchain.deps_mode() == file_deps_mode::msvc) { // Uglier deps generation by parsing the output from cl.exe - dds_log(trace, "Parsing compilation dependencies from MSVC output"); + bpt_log(trace, "Parsing compilation dependencies from MSVC output"); /// TODO: Handle different #include Note: prefixes, since those are localized auto msvc_deps = parse_msvc_output_for_deps(compiler_output, "Note: including file:"); // parse_msvc_output_for_deps will return the compile output without the /showIncludes notes @@ -168,6 +174,11 @@ handle_compilation(const compile_ticket& compile, build_env_ref env, compile_cou */ } + if (ret_deps_info) { + ret_deps_info->command.toolchain_hash = env.toolchain.hash(); + ret_deps_info->compile_start_time = start_time; + } + // MSVC prints the filename of the source file. Remove it from the output. if (compiler_output.find(source_path.filename().string()) == 0) { compiler_output.erase(0, source_path.filename().string().length()); @@ -181,21 +192,26 @@ handle_compilation(const compile_ticket& compile, build_env_ref env, compile_cou // Log a compiler failure if (!compiled_okay) { - dds_log(error, "Compilation failed: .bold.cyan[{}]"_styled, source_path.string()); - dds_log(error, + std::string_view compilation_failure_msg + = compile.is_syntax_only ? "Syntax check failed" : "Compilation failed"; + bpt_log(error, "{}: .bold.cyan[{}]"_styled, compilation_failure_msg, source_path.string()); + bpt_log(error, "Subcommand .bold.red[FAILED] [Exited {}]: .bold.yellow[{}]\n{}"_styled, compile_retc, quote_command(compile.command.command), compiler_output); if (compile_signal) { - dds_log(error, "Process exited via signal {}", compile_signal); + bpt_log(error, "Process exited via signal {}", compile_signal); } - throw_user_error("Compilation failed [{}]", source_path.string()); + /// XXX: Use different error based on if a syntax-only check failed + throw_user_error("{} [{}]", + compilation_failure_msg, + source_path.string()); } // Print any compiler output, sans whitespace - if (!dds::trim_view(compiler_output).empty()) { - dds_log(warn, + if (!bpt::trim_view(compiler_output).empty()) { + bpt_log(warn, "While compiling file .bold.cyan[{}] [.bold.yellow[{}]]:\n{}"_styled, source_path.string(), quote_command(compile.command.command), @@ -216,29 +232,33 @@ compile_ticket mk_compile_ticket(const compile_file_plan& plan, build_env_ref en .command = plan.generate_compile_command(env), .object_file_path = plan.calc_object_file_path(env), .needs_recompile = false, - .prior_command = {}}; + .prior_command = {}, + .is_syntax_only = plan.rules().syntax_only()}; auto rb_info = get_prior_compilation(env.db, ret.object_file_path); if (!rb_info) { - dds_log(trace, "Compile {}: No recorded compilation info", plan.source_path().string()); + bpt_log(trace, "Compile {}: No recorded compilation info", plan.source_path().string()); ret.needs_recompile = true; - } else if (!fs::exists(ret.object_file_path)) { - dds_log(trace, "Compile {}: Output does not exist", plan.source_path().string()); + } else if (!fs::exists(ret.object_file_path) && !ret.is_syntax_only) { + bpt_log(trace, "Compile {}: Output does not exist", plan.source_path().string()); // The output file simply doesn't exist. We have to recompile, of course. ret.needs_recompile = true; } else if (!rb_info->newer_inputs.empty()) { // Inputs to this file have changed from a prior execution. - dds_log(trace, + bpt_log(trace, "Recompile {}: Inputs have changed (or no input information)", plan.source_path().string()); + for (auto& in : rb_info->newer_inputs) { + bpt_log(trace, " - Newer input: [{}]", in.string()); + } ret.needs_recompile = true; } else if (quote_command(ret.command.command) != rb_info->previous_command.quoted_command) { - dds_log(trace, "Recompile {}: Compile command has changed", plan.source_path().string()); + bpt_log(trace, "Recompile {}: Compile command has changed", plan.source_path().string()); // The command used to generate the output is new ret.needs_recompile = true; } else { // Nope. This file is up-to-date. - dds_log(debug, + bpt_log(debug, "Skip compilation of {} (Result is up-to-date)", plan.source_path().string()); } @@ -250,7 +270,7 @@ compile_ticket mk_compile_ticket(const compile_file_plan& plan, build_env_ref en } // namespace -bool dds::detail::compile_all(const ref_vector& compiles, +bool bpt::detail::compile_all(const ref_vector& compiles, build_env_ref env, int njobs) { auto each_realized = // @@ -280,13 +300,13 @@ bool dds::detail::compile_all(const ref_vector& compile }); // Update compile dependency information - dds::stopwatch update_timer; + bpt::stopwatch update_timer; auto tr = env.db.transaction(); for (auto& info : all_new_deps) { - dds_log(trace, "Update dependency info on {}", info.output.string()); + bpt_log(trace, "Update dependency info on {}", info.output.string()); update_deps_info(neo::into(env.db), info); } - dds_log(debug, "Dependency update took {:L}ms", update_timer.elapsed_ms().count()); + bpt_log(debug, "Dependency update took {:L}ms", update_timer.elapsed_ms().count()); cancellation_point(); // Return whether or not there were any failures. diff --git a/src/dds/build/plan/compile_exec.hpp b/src/bpt/build/plan/compile_exec.hpp similarity index 85% rename from src/dds/build/plan/compile_exec.hpp rename to src/bpt/build/plan/compile_exec.hpp index 906940d6..1c924a35 100644 --- a/src/dds/build/plan/compile_exec.hpp +++ b/src/bpt/build/plan/compile_exec.hpp @@ -1,13 +1,13 @@ #pragma once -#include -#include -#include +#include +#include +#include #include #include -namespace dds { +namespace bpt { namespace detail { @@ -32,4 +32,4 @@ bool compile_all(Range&& rng, build_env_ref env, int njobs) { return detail::compile_all(cfps, env, njobs); } -} // namespace dds \ No newline at end of file +} // namespace bpt \ No newline at end of file diff --git a/src/dds/build/plan/compile_file.cpp b/src/bpt/build/plan/compile_file.cpp similarity index 77% rename from src/dds/build/plan/compile_file.cpp rename to src/bpt/build/plan/compile_file.cpp index c479903f..d9f27e22 100644 --- a/src/dds/build/plan/compile_file.cpp +++ b/src/bpt/build/plan/compile_file.cpp @@ -1,9 +1,9 @@ #include "./compile_file.hpp" -#include -#include -#include -#include +#include +#include +#include +#include #include #include @@ -11,16 +11,14 @@ #include #include -using namespace dds; +using namespace bpt; compile_command_info compile_file_plan::generate_compile_command(build_env_ref env) const { compile_file_spec spec{_source.path, calc_object_file_path(env)}; spec.enable_warnings = _rules.enable_warnings(); + spec.syntax_only = _rules.syntax_only(); for (auto dirpath : _rules.include_dirs()) { - if (!dirpath.is_absolute()) { - dirpath = env.output_root / dirpath; - } - dirpath = fs::weakly_canonical(dirpath); + dirpath = bpt::resolve_path_weak(dirpath); spec.include_dirs.push_back(std::move(dirpath)); } for (const auto& use : _rules.uses()) { @@ -30,7 +28,7 @@ compile_command_info compile_file_plan::generate_compile_command(build_env_ref e // Avoid huge command lines by shrinking down the list of #include dirs sort_unique_erase(spec.external_include_dirs); sort_unique_erase(spec.include_dirs); - return env.toolchain.create_compile_command(spec, dds::fs::current_path(), env.knobs); + return env.toolchain.create_compile_command(spec, bpt::fs::current_path(), env.knobs); } fs::path compile_file_plan::calc_object_file_path(const build_env& env) const noexcept { diff --git a/src/dds/build/plan/compile_file.hpp b/src/bpt/build/plan/compile_file.hpp similarity index 91% rename from src/dds/build/plan/compile_file.hpp rename to src/bpt/build/plan/compile_file.hpp index c5a2e6c3..34768064 100644 --- a/src/dds/build/plan/compile_file.hpp +++ b/src/bpt/build/plan/compile_file.hpp @@ -1,13 +1,13 @@ #pragma once -#include -#include +#include +#include #include #include -namespace dds { +namespace bpt { /** * Exception thrown to indicate a compile failure @@ -29,6 +29,7 @@ class shared_compile_file_rules { std::vector defs; std::vector uses; bool enable_warnings = false; + bool syntax_only = false; }; /// The actual PIMPL. @@ -67,6 +68,12 @@ class shared_compile_file_rules { */ auto& enable_warnings() noexcept { return _impl->enable_warnings; } auto& enable_warnings() const noexcept { return _impl->enable_warnings; } + + /** + * A boolean to toggle syntax-only compilation + */ + auto& syntax_only() noexcept { return _impl->syntax_only; } + auto& syntax_only() const noexcept { return _impl->syntax_only; } }; /** @@ -129,4 +136,4 @@ class compile_file_plan { compile_command_info generate_compile_command(build_env_ref) const; }; -} // namespace dds \ No newline at end of file +} // namespace bpt \ No newline at end of file diff --git a/src/dds/build/plan/exe.cpp b/src/bpt/build/plan/exe.cpp similarity index 79% rename from src/dds/build/plan/exe.cpp rename to src/bpt/build/plan/exe.cpp index 17fc33f6..22aef05f 100644 --- a/src/dds/build/plan/exe.cpp +++ b/src/bpt/build/plan/exe.cpp @@ -1,18 +1,18 @@ #include "./exe.hpp" -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include #include #include #include -using namespace dds; +using namespace bpt; using namespace fansi::literals; fs::path link_executable_plan::calc_executable_path(build_env_ref env) const noexcept { @@ -24,39 +24,39 @@ void link_executable_plan::link(build_env_ref env, const library_plan& lib) cons link_exe_spec spec; spec.output = calc_executable_path(env); - dds_log(debug, "Performing link for {}", spec.output.string()); + bpt_log(debug, "Performing link for {}", spec.output.string()); // The main object should be a linker input, of course. auto main_obj = _main_compile.calc_object_file_path(env); - dds_log(trace, "Add entry point object file: {}", main_obj.string()); + bpt_log(trace, "Add entry point object file: {}", main_obj.string()); spec.inputs.push_back(std::move(main_obj)); if (lib.archive_plan()) { // The associated library has compiled components. Add the static library a as a linker // input - dds_log(trace, "Adding the library's archive as a linker input"); + bpt_log(trace, "Adding the library's archive as a linker input"); spec.inputs.push_back(env.output_root / lib.archive_plan()->calc_archive_file_path(env.toolchain)); } else { - dds_log(trace, "Executable has no corresponding archive library input"); + bpt_log(trace, "Executable has no corresponding archive library input"); } for (const lm::usage& links : _links) { - dds_log(trace, " - Link with: {}/{}", links.name, links.namespace_); + bpt_log(trace, " - Link with: {}/{}", links.name, links.namespace_); extend(spec.inputs, env.ureqs.link_paths(links)); } // Do it! const auto link_command - = env.toolchain.create_link_executable_command(spec, dds::fs::current_path(), env.knobs); + = env.toolchain.create_link_executable_command(spec, bpt::fs::current_path(), env.knobs); fs::create_directories(spec.output.parent_path()); auto msg = fmt::format("[{}] Link: {:30}", lib.qualified_name(), fs::relative(spec.output, env.output_root).string()); - dds_log(info, msg); + bpt_log(info, msg); auto [dur_ms, proc_res] = timed([&] { return run_proc(link_command); }); - dds_log(info, "{} - {:>6L}ms", msg, dur_ms.count()); + bpt_log(info, "{} - {:>6L}ms", msg, dur_ms.count()); // Check and throw if errant if (!proc_res.okay()) { @@ -82,19 +82,19 @@ std::optional link_executable_plan::run_test(build_env_ref env) co auto exe_path = calc_executable_path(env); auto msg = fmt::format("Run test: .br.cyan[{:30}]"_styled, fs::relative(exe_path, env.output_root).string()); - dds_log(info, msg); + bpt_log(info, msg); using namespace std::chrono_literals; auto&& [dur, res] = timed( [&] { return run_proc({.command = {exe_path.string()}, .timeout = 10s}); }); if (res.okay()) { - dds_log(info, "{} - .br.green[PASS] - {:>9L}μs"_styled, msg, dur.count()); + bpt_log(info, "{} - .br.green[PASS] - {:>9L}μs"_styled, msg, dur.count()); return std::nullopt; } else { auto exit_msg = fmt::format(res.signal ? "signalled {}" : "exited {}", res.signal ? res.signal : res.retc); auto fail_str = res.timed_out ? ".br.yellow[TIME]"_styled : ".br.red[FAIL]"_styled; - dds_log(error, "{} - {} - {:>9L}μs [{}]", msg, fail_str, dur.count(), exit_msg); + bpt_log(error, "{} - {} - {:>9L}μs [{}]", msg, fail_str, dur.count(), exit_msg); test_failure f; f.executable_path = exe_path; f.output = res.output; diff --git a/src/dds/build/plan/exe.hpp b/src/bpt/build/plan/exe.hpp similarity index 94% rename from src/dds/build/plan/exe.hpp rename to src/bpt/build/plan/exe.hpp index 3c8178df..97c85704 100644 --- a/src/dds/build/plan/exe.hpp +++ b/src/bpt/build/plan/exe.hpp @@ -1,14 +1,14 @@ #pragma once -#include -#include +#include +#include #include #include #include -namespace dds { +namespace bpt { class library_plan; @@ -24,7 +24,7 @@ struct test_failure { }; /** - * Stores information about an executable that should be linked. An executable in DDS consists of a + * Stores information about an executable that should be linked. An executable in BPT consists of a * single source file defines the entry point and some set of linker inputs. */ class link_executable_plan { @@ -81,4 +81,4 @@ class link_executable_plan { bool is_app() const noexcept; }; -} // namespace dds \ No newline at end of file +} // namespace bpt \ No newline at end of file diff --git a/src/dds/build/plan/full.cpp b/src/bpt/build/plan/full.cpp similarity index 70% rename from src/dds/build/plan/full.cpp rename to src/bpt/build/plan/full.cpp index 03552566..77a126ba 100644 --- a/src/dds/build/plan/full.cpp +++ b/src/bpt/build/plan/full.cpp @@ -1,11 +1,17 @@ #include "./full.hpp" -#include -#include -#include -#include -#include - +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include #include #include #include @@ -18,7 +24,7 @@ #include #include -using namespace dds; +using namespace bpt; namespace { @@ -28,22 +34,17 @@ decltype(auto) pair_up(T& left, Range& right) { return ranges::views::zip(rep, right); } -} // namespace +struct pending_file { + bool marked = false; + fs::path filepath; -void build_plan::render_all(build_env_ref env) const { - auto templates = _packages // - | ranges::views::transform(&package_plan::libraries) // - | ranges::views::join // - | ranges::views::transform( - [](const auto& lib) { return pair_up(lib, lib.templates()); }) // - | ranges::views::join; - for (const auto& [lib, tmpl] : templates) { - tmpl.render(env, lib.library_()); - } -} + auto operator<=>(const pending_file&) const noexcept = default; +}; + +} // namespace void build_plan::compile_all(const build_env& env, int njobs) const { - auto okay = dds::compile_all(iter_compilations(*this), env, njobs); + auto okay = bpt::compile_all(iter_compilations(*this), env, njobs); if (!okay) { throw_user_error(); } @@ -52,10 +53,6 @@ void build_plan::compile_all(const build_env& env, int njobs) const { void build_plan::compile_files(const build_env& env, int njobs, const std::vector& filepaths) const { - struct pending_file { - bool marked = false; - fs::path filepath; - }; auto as_pending = // ranges::views::all(filepaths) // @@ -64,6 +61,8 @@ void build_plan::compile_files(const build_env& env, }) | ranges::to_vector; + bpt::sort_unique_erase(as_pending); + auto check_compilation = [&](const compile_file_plan& comp) { return ranges::any_of(as_pending, [&](pending_file& f) { bool same_file = f.filepath == fs::weakly_canonical(comp.source_path()); @@ -74,24 +73,24 @@ void build_plan::compile_files(const build_env& env, }); }; + // Create a vector of compilations, and mark files so that we can find who hasn't been marked. auto comps = iter_compilations(*this) | ranges::views::filter(check_compilation) | ranges::to_vector; - bool any_unmarked = false; - auto unmarked = ranges::views::filter(as_pending, ranges::not_fn(&pending_file::marked)); - for (auto&& um : unmarked) { - dds_log(error, "Source file [{}] is not compiled by this project", um.filepath.string()); - any_unmarked = true; - } + // Make an error if there are any unmarked files + auto missing_files = as_pending // + | ranges::views::filter(BPT_TL(!_1.marked)) + | ranges::views::transform(BPT_TL(e_nonesuch{_1.filepath.string(), std::nullopt})) + | ranges::to_vector; - if (any_unmarked) { - throw_user_error( - "One or more requested files is not part of this project (See above)"); + if (!missing_files.empty()) { + BOOST_LEAF_THROW_EXCEPTION(make_user_error(), missing_files); } - auto okay = dds::compile_all(comps, env, njobs); + auto okay = bpt::compile_all(comps, env, njobs); if (!okay) { - throw_user_error(); + BOOST_LEAF_THROW_EXCEPTION(make_user_error(), + BPT_ERR_REF("compile-failure")); } } @@ -122,7 +121,8 @@ void build_plan::link_all(const build_env& env, int njobs) const { exe.get().link(env, lib); }); if (!okay) { - throw_user_error(); + BOOST_LEAF_THROW_EXCEPTION(make_user_error(), + BPT_ERR_REF("link-failure")); } } diff --git a/src/dds/build/plan/full.hpp b/src/bpt/build/plan/full.hpp similarity index 86% rename from src/dds/build/plan/full.hpp rename to src/bpt/build/plan/full.hpp index f2076f5b..76506f1a 100644 --- a/src/dds/build/plan/full.hpp +++ b/src/bpt/build/plan/full.hpp @@ -1,9 +1,9 @@ #pragma once -#include -#include +#include +#include -namespace dds { +namespace bpt { /** * Encompases an entire build plan. @@ -28,10 +28,6 @@ class build_plan { * All of the packages in this plan */ auto& packages() const noexcept { return _packages; } - /** - * Render all config templates in the plan. - */ - void render_all(const build_env& env) const; /** * Compile all files in the plan. */ @@ -56,4 +52,4 @@ class build_plan { std::vector run_all_tests(build_env_ref env, int njobs) const; }; -} // namespace dds +} // namespace bpt diff --git a/src/bpt/build/plan/library.cpp b/src/bpt/build/plan/library.cpp new file mode 100644 index 00000000..4629d093 --- /dev/null +++ b/src/bpt/build/plan/library.cpp @@ -0,0 +1,213 @@ +#include "./library.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace bpt; +using namespace fansi::literals; + +library_plan library_plan::create(path_ref pkg_base, + const crs::package_info& pkg, + const crs::library_info& lib, + const library_build_params& params) { + fs::path out_dir = params.out_subdir; + auto qual_name = neo::ufmt("{}/{}", pkg.id.name.str, lib.name.str); + + // Source files are kept in different groups: + std::vector app_sources; + std::vector test_sources; + std::vector lib_sources; + std::vector header_sources; + std::vector public_header_sources; + + // Collect the source for this library. This will look for any compilable sources in the + // `src/` subdirectory of the library. + auto src_dir = bpt::source_root(pkg_base / lib.path / "src"); + if (src_dir.exists()) { + // Sort each source file between the three source arrays, depending on + // the kind of source that we are looking at. + auto all_sources = src_dir.collect_sources(); + for (const auto& sfile : all_sources) { + if (sfile.kind == source_kind::test) { + test_sources.push_back(sfile); + } else if (sfile.kind == source_kind::app) { + app_sources.push_back(sfile); + } else if (sfile.kind == source_kind::source) { + lib_sources.push_back(sfile); + } else if (sfile.kind == source_kind::header) { + header_sources.push_back(sfile); + } else { + assert(sfile.kind == source_kind::header_impl); + } + } + } + + auto include_dir = bpt::source_root{pkg_base / lib.path / "include"}; + if (include_dir.exists()) { + auto all_sources = include_dir.collect_sources(); + for (const auto& sfile : all_sources) { + if (!is_header(sfile.kind)) { + bpt_log( + warn, + "Public include/ should only contain header files. Not a header: [.br.yellow[{}]]"_styled, + sfile.path.string()); + } else if (sfile.kind == source_kind::header) { + public_header_sources.push_back(sfile); + } + } + } + if (!params.build_tests) { + public_header_sources.clear(); + header_sources.clear(); + } + + auto pkg_dep_to_usages = [&](crs::dependency const& dep) { + return dep.uses | std::views::transform([&](auto&& l) { + return lm::usage{dep.name.str, l.str}; + }); + }; + + auto usages_of_kind + = [&](auto intra_ptr, auto deps_ptr) -> neo::ranges::range_of auto { + auto intra = std::invoke(intra_ptr, lib) // + | std::views::transform([&](auto& use) { + return lm::usage{pkg.id.name.str, use.str}; + }); + auto from_dep // + = std::invoke(deps_ptr, lib) // + | std::views::transform(pkg_dep_to_usages) // + | std::views::join; + neo::ranges::range_of auto ret_uses = ranges::views::concat(intra, from_dep); + return ret_uses | neo::to_vector; + }; + + auto lib_uses + = usages_of_kind(&crs::library_info::intra_using, &crs::library_info::dependencies); + auto test_uses = usages_of_kind(&crs::library_info::intra_test_using, + &crs::library_info::test_dependencies); + + // Load up the compile rules + shared_compile_file_rules compile_rules; + auto& pub_inc = include_dir.exists() ? include_dir.path : src_dir.path; + compile_rules.include_dirs().push_back(pub_inc); + compile_rules.enable_warnings() = params.enable_warnings; + extend(compile_rules.uses(), lib_uses); + + auto public_header_compile_rules = compile_rules.clone(); + public_header_compile_rules.syntax_only() = true; + public_header_compile_rules.defs().push_back("__bpt_header_check=1"); + auto src_header_compile_rules = public_header_compile_rules.clone(); + if (include_dir.exists()) { + compile_rules.include_dirs().push_back(src_dir.path); + src_header_compile_rules.include_dirs().push_back(src_dir.path); + } + + // Convert the library sources into their respective file compilation plans. + auto lib_compile_files = // + lib_sources // + | ranges::views::transform([&](const source_file& sf) { + return compile_file_plan(compile_rules, sf, qual_name, out_dir / "obj"); + }) + | ranges::to_vector; + + // Run a syntax-only pass over headers to verify that headers can build in isolation. + auto header_indep_plan = header_sources // + | ranges::views::transform([&](const source_file& sf) { + return compile_file_plan(src_header_compile_rules, + sf, + qual_name, + out_dir / "timestamps"); + }) + | ranges::to_vector; + extend(header_indep_plan, + public_header_sources | ranges::views::transform([&](const source_file& sf) { + return compile_file_plan(public_header_compile_rules, + sf, + qual_name, + out_dir / "timestamps"); + })); + // If we have any compiled library files, generate a static library archive + // for this library + std::optional archive_plan; + if (!lib_compile_files.empty()) { + bpt_log(debug, "Generating an archive library for {}", qual_name); + archive_plan.emplace(lib.name.str, qual_name, out_dir, std::move(lib_compile_files)); + } else { + bpt_log(debug, + "Library {} has no compiled inputs, so no archive will be generated", + qual_name); + } + + // Collect the paths to linker inputs that should be used when generating executables for + // this library. + std::vector links; + extend(links, lib_uses); + + // There may also be additional usage requirements for tests + auto test_rules = compile_rules.clone(); + auto test_links = links; + extend(test_rules.uses(), test_uses); + extend(test_links, test_uses); + + // Generate the plans to link any executables for this library + std::vector link_executables; + for (const source_file& source : ranges::views::concat(app_sources, test_sources)) { + const bool is_test = source.kind == source_kind::test; + if (is_test && !params.build_tests) { + // This is a test, but we don't want to build tests + continue; + } + if (!is_test && !params.build_apps) { + // This is an app, but we don't want to build apps + continue; + } + // Pick a subdir based on app/test + const auto subdir_base = is_test ? out_dir / "test" : out_dir; + // Put test/app executables in a further subdirectory based on the source file path + const auto subdir = subdir_base / source.relative_path().parent_path(); + // Pick compile rules based on app/test + auto rules = is_test ? test_rules : compile_rules; + // Pick input libs based on app/test + auto& exe_links = is_test ? test_links : links; + // TODO: Apps/tests should only see the _public_ include dir, not both + auto exe + = link_executable_plan{exe_links, + compile_file_plan(rules, source, qual_name, out_dir / "obj"), + subdir, + source.path.stem().stem().string()}; + link_executables.emplace_back(std::move(exe)); + } + + // Done! + return library_plan{lib.name.str, + pkg_base / lib.path, + qual_name, + out_dir, + std::move(archive_plan), + std::move(link_executables), + std::move(header_indep_plan), + std::move(lib_uses)}; +} + +fs::path library_plan::public_include_dir() const noexcept { + auto p = source_root() / "include"; + if (fs::exists(p)) { + return p; + } + return source_root() / "src"; +} diff --git a/src/dds/build/plan/library.hpp b/src/bpt/build/plan/library.hpp similarity index 68% rename from src/dds/build/plan/library.hpp rename to src/bpt/build/plan/library.hpp index c14fb270..3f8ddc96 100644 --- a/src/dds/build/plan/library.hpp +++ b/src/bpt/build/plan/library.hpp @@ -1,11 +1,11 @@ #pragma once -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include + +#include #include @@ -13,12 +13,14 @@ #include #include -namespace dds { +namespace bpt { /** * The parameters that tweak the behavior of building a library */ struct library_build_params { + /// The root directory of the library + fs::path root; /// The subdirectory of the build root in which this library should place its files. fs::path out_subdir; /// Whether tests should be compiled and linked for this library @@ -27,12 +29,6 @@ struct library_build_params { bool build_apps = false; /// Whether compiler warnings should be enabled for building the source files in this library. bool enable_warnings = false; - - /// Directories that should be on the #include search path when compiling tests - std::vector test_include_dirs; - - /// Libraries that are used by tests - std::vector test_uses; }; /** @@ -51,8 +47,8 @@ struct library_build_params { * initialize all of the constructor parameters correctly. */ class library_plan { - /// The underlying library root - library_root _lib; + std::string _name; + fs::path _lib_root; /// The qualified name of the library std::string _qual_name; /// The library's subdirectory within the output directory @@ -61,8 +57,10 @@ class library_plan { std::optional _create_archive; /// The executables that should be linked as part of this library's build std::vector _link_exes; - /// The templates that must be rendered for this library - std::vector _templates; + /// The headers that must be checked for independence + std::vector _headers; + /// Libraries used by this library + std::vector _lib_uses; public: /** @@ -71,27 +69,23 @@ class library_plan { * @param ar The `create_archive_plan`, or `nullopt` for this library. * @param exes The `link_executable_plan` objects for this library. */ - library_plan(library_root lib, + library_plan(std::string name, + path_ref lib_root, std::string_view qual_name, fs::path subdir, std::optional ar, std::vector exes, - std::vector tmpls) - : _lib(std::move(lib)) + std::vector headers, + std::vector lib_uses) + : _name(name) + , _lib_root(lib_root) , _qual_name(qual_name) , _subdir(std::move(subdir)) , _create_archive(std::move(ar)) , _link_exes(std::move(exes)) - , _templates(std::move(tmpls)) {} - - /** - * Get the underlying library object - */ - auto& library_() const noexcept { return _lib; } - /** - * Get the name of the library - */ - auto& name() const noexcept { return _lib.manifest().name; } + , _headers(std::move(headers)) + , _lib_uses(std::move(lib_uses)) {} + std::string_view name() const noexcept { return _name; } /** * Get the qualified name of the library, as if for a libman usage requirement */ @@ -103,34 +97,28 @@ class library_plan { /** * The directory that defines the source root of the library. */ - path_ref source_root() const noexcept { return _lib.path(); } + path_ref source_root() const noexcept { return _lib_root; } /** * A `create_archive_plan` object, or `nullopt`, depending on if this library has compiled * components */ auto& archive_plan() const noexcept { return _create_archive; } - /** - * The template rendering plans for this library. - */ - auto& templates() const noexcept { return _templates; } /** * The executables that should be created by this library */ auto& executables() const noexcept { return _link_exes; } /** - * The library identifiers that are used by this library + * The headers that should be checked for independence by this library */ - auto& uses() const noexcept { return _lib.manifest().uses; } + auto& headers() const noexcept { return _headers; } /** - * The library identifiers that are linked by this library + * The library identifiers that are used by this library */ - auto& links() const noexcept { return _lib.manifest().links; } + auto& lib_uses() const noexcept { return _lib_uses; } /** - * The path to the directory that should be added for the #include search - * path for this library, relative to the build root. Returns `nullopt` if - * this library has no generated headers. + * @brief The public header source root directory for this library */ - std::optional generated_include_dir() const noexcept; + fs::path public_include_dir() const noexcept; /** * Named constructor: Create a new `library_plan` automatically from some build-time parameters. @@ -145,9 +133,10 @@ class library_plan { * The `lib` parameter defines the usage requirements of this library, and they are looked up in * the `ureqs` map. If there are any missing requirements, an exception will be thrown. */ - static library_plan create(const library_root& lib, - const library_build_params& params, - std::optional qual_name); + static library_plan create(path_ref pkg_base, + const crs::package_info& pkg, + const crs::library_info& lib, + const library_build_params& params); }; -} // namespace dds +} // namespace bpt diff --git a/src/dds/build/plan/package.hpp b/src/bpt/build/plan/package.hpp similarity index 66% rename from src/dds/build/plan/package.hpp rename to src/bpt/build/plan/package.hpp index 95233b35..2784285f 100644 --- a/src/dds/build/plan/package.hpp +++ b/src/bpt/build/plan/package.hpp @@ -1,11 +1,11 @@ #pragma once -#include +#include #include #include -namespace dds { +namespace bpt { /** * A package is a top-level component with a name, namespace, and some number of associated @@ -15,8 +15,6 @@ namespace dds { class package_plan { /// Package name std::string _name; - /// The package namespace. Used to specify interdependencies - std::string _namespace; /// The libraries in this package std::vector _libraries; @@ -24,11 +22,9 @@ class package_plan { /** * Create a new package plan. * @param name The name of the package - * @param namespace_ The namespace of the package. Used when specifying linker dependencies. */ - package_plan(std::string_view name, std::string_view namespace_) - : _name(name) - , _namespace(namespace_) {} + package_plan(std::string_view name) + : _name(name) {} /** * Add a library plan to this package plan @@ -41,14 +37,10 @@ class package_plan { * Get the package name */ auto& name() const noexcept { return _name; } - /** - * The package namespace - */ - auto& namespace_() const noexcept { return _namespace; } /** * The libraries in the package */ auto& libraries() const noexcept { return _libraries; } }; -} // namespace dds \ No newline at end of file +} // namespace bpt \ No newline at end of file diff --git a/src/bpt/cli/cmd/build.cpp b/src/bpt/cli/cmd/build.cpp new file mode 100644 index 00000000..5647302d --- /dev/null +++ b/src/bpt/cli/cmd/build.cpp @@ -0,0 +1,30 @@ +#include "../options.hpp" + +#include "./build_common.hpp" + +#include +#include +#include + +using namespace bpt; + +namespace bpt::cli::cmd { + +static int _build(const options& opts) { + auto builder = create_project_builder(opts); + builder.build({ + .out_root = opts.out_path.value_or(fs::current_path() / "_build"), + .emit_built_json = std::nullopt, + .tweaks_dir = opts.build.tweaks_dir, + .toolchain = opts.load_toolchain(), + .parallel_jobs = opts.jobs, + }); + + return 0; +} + +int build(const options& opts) { + return handle_build_error([&] { return _build(opts); }); +} + +} // namespace bpt::cli::cmd diff --git a/src/bpt/cli/cmd/build_common.cpp b/src/bpt/cli/cmd/build_common.cpp new file mode 100644 index 00000000..8294acc1 --- /dev/null +++ b/src/bpt/cli/cmd/build_common.cpp @@ -0,0 +1,151 @@ +#include "./build_common.hpp" + +#include "./cache_util.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace bpt; +using namespace fansi::literals; + +builder bpt::cli::create_project_builder(const bpt::cli::options& opts) { + sdist_build_params main_params = { + .subdir = "", + .build_tests = opts.build.want_tests, + .run_tests = opts.build.want_tests, + .build_apps = opts.build.want_apps, + .enable_warnings = !opts.disable_warnings, + .build_libraries = {}, + }; + + auto cache = open_ready_cache(opts); + auto& meta_db = cache.db(); + + sdist proj_sd = bpt_leaf_try { return sdist::from_directory(opts.absolute_project_dir_path()); } + bpt_leaf_catch(bpt::e_missing_pkg_json, bpt::e_missing_project_yaml) { + crs::package_info default_meta; + default_meta.id.name.str = "anon"; + default_meta.id.revision = 0; + crs::library_info default_library; + default_library.name.str = "anon"; + default_meta.libraries.push_back(default_library); + return sdist{std::move(default_meta), opts.absolute_project_dir_path()}; + }; + + builder builder; + if (!opts.build.built_json.has_value()) { + auto crs_deps = proj_sd.pkg.libraries | std::views::transform(BPT_TL(_1.dependencies)) + | std::views::join | neo::to_vector; + + if (opts.build.want_tests) { + extend(crs_deps, + proj_sd.pkg.libraries | std::views::transform(BPT_TL(_1.test_dependencies)) + | std::views::join); + } + + auto sln = bpt::solve(meta_db, crs_deps); + for (auto&& pkg : sln) { + fetch_cache_load_dependency(cache, + pkg, + false /* Do not mark libraries to be built */, + builder, + "_deps"); + } + } + + extend(main_params.build_libraries, + proj_sd.pkg.libraries | std::views::transform(&crs::library_info::name)); + builder.add(proj_sd, main_params); + return builder; +} + +crs::package_info bpt::cli::fetch_cache_load_dependency(crs::cache& cache, + crs::pkg_id const& pkg, + bool build_all_libs, + bpt::builder& builder, + path_ref subdir_base) { + bpt_log(debug, "Loading package '{}' for build", pkg.to_string()); + auto local_dir = cache.prefetch(pkg); + auto pkg_json_path = local_dir / "pkg.json"; + auto pkg_json_content = bpt::read_file(pkg_json_path); + BPT_E_SCOPE(crs::e_pkg_json_path{pkg_json_path}); + auto crs_meta = crs::package_info::from_json_str(pkg_json_content); + + bpt::sdist sd{crs_meta, local_dir}; + sdist_build_params params; + if (build_all_libs) { + extend(params.build_libraries, + crs_meta.libraries | std::views::transform(&crs::library_info::name)); + } + params.subdir = subdir_base / sd.pkg.id.to_string(); + builder.add(sd, params); + return crs_meta; +} + +int bpt::cli::handle_build_error(std::function fn) { + return bpt_leaf_try { return fn(); } + bpt_leaf_catch(e_dependency_solve_failure, + e_dependency_solve_failure_explanation explain, + const std::vector* missing_pkgs, + const std::vector* missing_libs) + ->int { + bpt_log( + error, + "No dependency solution is possible with the known package information: \n{}"_styled, + explain.value); + if (missing_pkgs) { + for (auto& missing : *missing_pkgs) { + missing.log_error( + "Direct requirement on '.bold.red[{}]' does not name an existing package in any enabled repositories"_styled); + } + } + if (missing_libs) { + for (auto& lib : *missing_libs) { + bpt_log( + error, + "There is no available version of .bold.yellow[{}] that contains a library named \".bold.red[{}]\""_styled, + lib.pkg_name.str, + lib.lib.given); + if (lib.lib.nearest) { + bpt_log(error, + " (Did you mean \".bold.yellow[{}]\"?)"_styled, + *lib.lib.nearest); + } + } + } + write_error_marker("no-dependency-solution"); + return 1; + } + bpt_leaf_catch(user_error)->int { + write_error_marker("compile-failed"); + throw; + } + bpt_leaf_catch(user_error)->int { + write_error_marker("link-failed"); + throw; + } + bpt_leaf_catch(user_error exc) { + write_error_marker("build-failed-test-failed"); + bpt_log(error, "{}", exc.what()); + bpt_log(error, "{}", exc.explanation()); + bpt_log(error, "Refer: {}", exc.error_reference()); + return 1; + }; +} diff --git a/src/bpt/cli/cmd/build_common.hpp b/src/bpt/cli/cmd/build_common.hpp new file mode 100644 index 00000000..495d144b --- /dev/null +++ b/src/bpt/cli/cmd/build_common.hpp @@ -0,0 +1,45 @@ +#include "../options.hpp" + +#include + +#include +#include + +namespace bpt::crs { + +class cache; +struct pkg_id; +struct package_info; + +} // namespace bpt::crs + +namespace bpt::cli { + +bpt::builder create_project_builder(const options& opts); + +int handle_build_error(std::function); + +/** + * @brief Fetch, cache, and load the given package ID. + * + * For a given package ID: + * + * - If it is not already locally cached, download the package data and store + * it in the local CRS cache. + * - Load the CRS source distribution into the given builder. + * + * The given package must have locally cached metadata for an enabled repository. + * + * @param cache + * @param pkg + * @param build_all_libs If `true`, all libraries will be marked for building, otherwise none + * @param b + * @return crs::package_info + */ +crs::package_info fetch_cache_load_dependency(crs::cache& cache, + const crs::pkg_id& pkg, + bool build_all_libs, + builder& b, + const std::filesystem::path& subdir_base); + +} // namespace bpt::cli diff --git a/src/bpt/cli/cmd/build_deps.cpp b/src/bpt/cli/cmd/build_deps.cpp new file mode 100644 index 00000000..67a2d8dd --- /dev/null +++ b/src/bpt/cli/cmd/build_deps.cpp @@ -0,0 +1,72 @@ +#include "../options.hpp" + +#include "./build_common.hpp" +#include "./cache_util.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace bpt::cli::cmd { + +static int _build_deps(const options& opts) { + auto cache = open_ready_cache(opts); + + bpt::build_params params{ + .out_root = opts.out_path.value_or(fs::current_path() / "_deps"), + .emit_built_json = opts.build.built_json.value_or("_built.json"), + .emit_cmake = opts.build_deps.cmake_file, + .tweaks_dir = opts.build.tweaks_dir, + .toolchain = opts.load_toolchain(), + .parallel_jobs = opts.jobs, + }; + + bpt::builder builder; + bpt::sdist_build_params sdist_params; + + neo::ranges::range_of auto file_deps + = opts.build_deps.deps_files // + | ranges::views::transform([&](auto dep_fpath) { + bpt_log(info, "Reading deps from {}", dep_fpath.string()); + bpt::dependency_manifest depman = bpt::dependency_manifest::from_file(dep_fpath); + return depman.dependencies + | std::views::transform(&project_dependency::as_crs_dependency) | neo::to_vector; + }) + | ranges::actions::join; + + neo::ranges::range_of auto cli_deps + = std::views::transform(opts.build_deps.deps, + BPT_TL(bpt::project_dependency::from_shorthand_string(_1) + .as_crs_dependency())); + + neo::ranges::range_of auto all_deps + = ranges::views::concat(file_deps, cli_deps); + + auto sln = bpt::solve(cache.db(), all_deps); + for (auto&& pkg : sln) { + fetch_cache_load_dependency(cache, + pkg, + true /* Build all libraries in the dependency */, + builder, + "."); + } + + builder.build(params); + return 0; +} + +int build_deps(const options& opts) { + return handle_build_error([&] { return _build_deps(opts); }); +} + +} // namespace bpt::cli::cmd diff --git a/src/bpt/cli/cmd/cache_util.cpp b/src/bpt/cli/cmd/cache_util.cpp new file mode 100644 index 00000000..531a6cd6 --- /dev/null +++ b/src/bpt/cli/cmd/cache_util.cpp @@ -0,0 +1,153 @@ +#include "./cache_util.hpp" + +#include "../options.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace bpt; +using namespace fansi::literals; + +static void +use_repo(bpt::crs::cache_db& meta_db, const cli::options& opts, std::string_view url_or_path) { + if (url_or_path == ":default") { + url_or_path = "https://repo-3.bpt.pizza/"; + } + // Convert what may be just a domain name or partial URL into a proper URL: + auto url = bpt::guess_url_from_string(url_or_path); + // Called by error handler to decide whether to rethrow: + auto check_cache_after_error = [&] { + if (opts.repo_sync_mode == cli::repo_sync_mode::always) { + // We should always sync package listings, so this is a hard error + bpt::throw_system_exit(1); + } + auto rid = meta_db.get_remote(url); + if (rid.has_value()) { + // We have prior metadata for the given repository. + bpt_log(warn, + "We'll continue by using cached information for .bold.yellow[{}]"_styled, + url.to_string()); + } else { + bpt_log( + error, + "We have no cached metadata for .bold.red[{}], and were unable to obtain any."_styled, + url.to_string()); + bpt::throw_system_exit(1); + } + }; + bpt_leaf_try { + using m = cli::repo_sync_mode; + switch (opts.repo_sync_mode) { + case m::cached_okay: + case m::always: + meta_db.sync_remote(url); + return; + case m::never: + return; + } + } + bpt_leaf_catch(matchv, + bpt::crs::e_sync_remote sync_repo, + neo::url req_url) { + bpt_log( + error, + "Received an .bold.red[HTTP 404 Not Found] error while synchronizing a repository from .bold.yellow[{}]"_styled, + sync_repo.value.to_string()); + bpt_log(error, + "The given location might not be a valid package repository, or the URL might be " + "spelled incorrectly."); + bpt_log(error, + " (The missing resource URL is [.bold.yellow[{}]])"_styled, + req_url.to_string()); + write_error_marker("repo-sync-http-404"); + check_cache_after_error(); + } + bpt_leaf_catch(catch_, + neo::url req_url, + bpt::crs::e_sync_remote sync_repo, + bpt::http_response_info resp) { + bpt_log( + error, + "HTTP .br.red[{}] (.br.red[{}]) error while trying to synchronize remote package repository [.bold.yellow[{}]]"_styled, + resp.status, + resp.status_message, + sync_repo.value.to_string()); + bpt_log(error, + " Error requesting [.bold.yellow[{}]]: .bold.red[HTTP {} {}]"_styled, + req_url.to_string(), + resp.status, + resp.status_message); + write_error_marker("repo-sync-http-error"); + check_cache_after_error(); + } + bpt_leaf_catch(catch_ exc, bpt::crs::e_sync_remote sync_repo) { + bpt_log(error, + "SQLite error while importing data from .br.yellow[{}]: .br.red[{}]"_styled, + sync_repo.value.to_string(), + exc.matched.what()); + bpt_log(error, + "It's possible that the downloaded SQLite database is corrupt, invalid, or " + "incompatible with this version of bpt"); + check_cache_after_error(); + } + bpt_leaf_catch(bpt::crs::e_sync_remote sync_repo, + bpt::e_decompress_error err, + bpt::e_read_file_path read_file) { + bpt_log(error, + "Error while sychronizing package data from .bold.yellow[{}]"_styled, + sync_repo.value.to_string()); + bpt_log( + error, + "Error decompressing remote repository database [.br.yellow[{}]]: .bold.red[{}]"_styled, + read_file.value.string(), + err.value); + write_error_marker("repo-sync-invalid-db-gz"); + check_cache_after_error(); + } + bpt_leaf_catch(const std::system_error& e, neo::url e_url, http_response_info) { + bpt_log(error, + "An error occurred while downloading [.bold.red[{}]]: {}"_styled, + e_url.to_string(), + e.code().message()); + check_cache_after_error(); + } + bpt_leaf_catch(const std::system_error& e, network_origin origin, neo::url const* e_url) { + bpt_log(error, + "Network error communicating with .bold.red[{}://{}:{}]: {}"_styled, + origin.protocol, + origin.hostname, + origin.port, + e.code().message()); + if (e_url) { + bpt_log(error, " (While accessing URL [.bold.red[{}]])"_styled, e_url->to_string()); + check_cache_after_error(); + } else { + check_cache_after_error(); + } + }; + meta_db.enable_remote(url); +} + +crs::cache cli::open_ready_cache(const cli::options& opts) { + auto cache = bpt::crs::cache::open(opts.crs_cache_dir); + auto& meta_db = cache.db(); + for (auto& r : opts.use_repos) { + use_repo(meta_db, opts, r); + } + if (opts.use_default_repo) { + use_repo(meta_db, opts, "repo-3.bpt.pizza"); + } + return cache; +} diff --git a/src/bpt/cli/cmd/cache_util.hpp b/src/bpt/cli/cmd/cache_util.hpp new file mode 100644 index 00000000..e34fc6f2 --- /dev/null +++ b/src/bpt/cli/cmd/cache_util.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +namespace bpt::cli { + +struct options; + +/** + * @brief Open a CRS cache in the appropriate directory, will all requested + * remote repositories synced and enabled. + * + * @param opts The options given by the user. + */ +bpt::crs::cache open_ready_cache(const options& opts); + +} // namespace bpt::cli diff --git a/src/bpt/cli/cmd/compile_file.cpp b/src/bpt/cli/cmd/compile_file.cpp new file mode 100644 index 00000000..30e228fb --- /dev/null +++ b/src/bpt/cli/cmd/compile_file.cpp @@ -0,0 +1,59 @@ +#include "../options.hpp" + +#include "./build_common.hpp" +#include +#include +#include +#include + +#include +#include + +using namespace fansi::literals; + +namespace bpt::cli::cmd { + +int _compile_file(const options& opts) { + return boost::leaf::try_catch( + [&] { + auto builder = create_project_builder(opts); + builder.compile_files(opts.compile_file.files, + { + .out_root + = opts.out_path.value_or(fs::current_path() / "_build"), + .emit_built_json = std::nullopt, + .tweaks_dir = opts.build.tweaks_dir, + .toolchain = opts.load_toolchain(), + .parallel_jobs = opts.jobs, + }); + return 0; + }, + [&](boost::leaf::catch_>, + std::vector missing) { + if (missing.size() == 1) { + bpt_log( + error, + "Requested source file [.bold.red[{}]] is not a file compiled for this project"_styled, + missing.front().given); + } else { + bpt_log(error, "The following files are not compiled as part of this project:"); + for (auto&& f : missing) { + bpt_log(error, " - .bold.red[{}]"_styled, f.given); + } + } + write_error_marker("nonesuch-compile-file"); + return 2; + }, + [&](boost::leaf::catch_> e) { + bpt_log(error, e.matched.what()); + bpt_log(error, " (Refer to compiler output for more information)"); + write_error_marker("compile-file-failed"); + return 2; + }); +} + +int compile_file(const options& opts) { + return handle_build_error([&] { return _compile_file(opts); }); +} + +} // namespace bpt::cli::cmd diff --git a/src/dds/cli/cmd/install_yourself.cpp b/src/bpt/cli/cmd/install_yourself.cpp similarity index 79% rename from src/dds/cli/cmd/install_yourself.cpp rename to src/bpt/cli/cmd/install_yourself.cpp index 61135b5a..645a1b46 100644 --- a/src/dds/cli/cmd/install_yourself.cpp +++ b/src/bpt/cli/cmd/install_yourself.cpp @@ -1,12 +1,13 @@ #include "../options.hpp" -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include -#include +#include #include #include #include @@ -15,6 +16,8 @@ #ifdef __APPLE__ #include #elif __FreeBSD__ +#include +// must come first #include #elif _WIN32 #include @@ -24,7 +27,7 @@ using namespace fansi::literals; -namespace dds::cli::cmd { +namespace bpt::cli::cmd { namespace { @@ -41,20 +44,22 @@ fs::path current_executable() { return fs::canonical(buffer); #elif __FreeBSD__ std::string buffer; - int mib[] = {CTRL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1}; - std::size_t len = 0; - auto rc = ::sysctl(mib, 4, nullptr, &len, nullptr, 0); + int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1}; + std::size_t len = 0; + auto rc = ::sysctl(mib, 4, nullptr, &len, nullptr, 0); + auto errno_ = errno; neo_assert(invariant, rc == 0, "Unexpected error from ::sysctl() while getting executable path", - errno); + errno_); buffer.resize(len + 1); - rc = ::sysctl(mib, 4, buffer.data(), &len, nullptr, 0); + rc = ::sysctl(mib, 4, buffer.data(), &len, nullptr, 0); + errno_ = errno; neo_assert(invariant, rc == 0, "Unexpected error from ::sysctl() while getting executable path", - errno); - return fs::canonical(nullptr); + errno_); + return fs::canonical(buffer); #elif _WIN32 std::wstring buffer; while (true) { @@ -74,9 +79,9 @@ fs::path current_executable() { fs::path user_binaries_dir() noexcept { #if _WIN32 - return dds::user_data_dir() / "bin"; + return bpt::user_data_dir() / "bin"; #else - return dds::user_home_dir() / ".local/bin"; + return bpt::user_home_dir() / ".local/bin"; #endif } @@ -128,15 +133,15 @@ void fixup_path_env(const options& opts, const wil::unique_hkey& env_hkey, fs::p auto path_elems = split_view(path_env_str, ";"); const bool any_match = std::any_of(path_elems.cbegin(), path_elems.cend(), [&](auto view) { auto existing = fs::weakly_canonical(view).make_preferred().lexically_normal(); - dds_log(trace, "Existing PATH entry: '{}'", existing.string()); + bpt_log(trace, "Existing PATH entry: '{}'", existing.string()); return existing.native() == want_entry.native(); }); if (any_match) { - dds_log(info, "PATH is up-to-date"); + bpt_log(info, "PATH is up-to-date"); return; } if (opts.dry_run) { - dds_log(info, "The PATH environment variable would be modified."); + bpt_log(info, "The PATH environment variable would be modified."); return; } // It's not there. Add it now. @@ -155,11 +160,11 @@ void fixup_path_env(const options& opts, const wil::unique_hkey& env_hkey, fs::p throw std::system_error(std::error_code(err, std::system_category()), "Failed to modify PATH environment variable"); } - dds_log( + bpt_log( info, "The directory [.br.cyan[{}]] has been added to your PATH environment variables."_styled, want_path.string()); - dds_log( + bpt_log( info, ".bold.cyan[NOTE:] You may need to restart running applications to see this change!"_styled); } @@ -187,21 +192,21 @@ void fixup_system_path(const options& opts [[maybe_unused]]) { void fixup_user_path(const options& opts) { #if !_WIN32 - auto profile_file = dds::user_home_dir() / ".profile"; - auto profile_content = dds::slurp_file(profile_file); - if (dds::contains(profile_content, "$HOME/.local/bin")) { + auto profile_file = bpt::user_home_dir() / ".profile"; + auto profile_content = bpt::read_file(profile_file); + if (bpt::contains(profile_content, "$HOME/.local/bin")) { // We'll assume that this is properly loading .local/bin for .profile - dds_log(info, "[.br.cyan[{}]] is okay"_styled, profile_file.string()); + bpt_log(info, "[.br.cyan[{}]] is okay"_styled, profile_file.string()); } else if (opts.dry_run) { - dds_log(info, + bpt_log(info, "Would update [.br.cyan[{}]] to have ~/.local/bin on $PATH"_styled, profile_file.string()); } else { // Let's add it profile_content - += ("\n# This entry was added by 'dds install-yourself' for the user-local " + += ("\n# This entry was added by 'bpt install-yourself' for the user-local " "binaries path\nPATH=$HOME/bin:$HOME/.local/bin:$PATH\n"); - dds_log(info, + bpt_log(info, "Updating [.br.cyan[{}]] with a user-local binaries PATH entry"_styled, profile_file.string()); auto tmp_file = profile_file; @@ -211,44 +216,44 @@ void fixup_user_path(const options& opts) { // Move .profile back into place if we abore for any reason neo_defer { if (!fs::exists(profile_file)) { - safe_rename(bak_file, profile_file); + move_file(bak_file, profile_file).value(); } }; // Write the temporary version - dds::write_file(tmp_file, profile_content).value(); + bpt::write_file(tmp_file, profile_content); // Make a backup - safe_rename(profile_file, bak_file); + move_file(profile_file, bak_file).value(); // Move the tmp over the final location - safe_rename(tmp_file, profile_file); + move_file(tmp_file, profile_file).value(); // Okay! - dds_log(info, + bpt_log(info, "[.br.green[{}]] was updated. Prior contents are safe in [.br.cyan[{}]]"_styled, profile_file.string(), bak_file.string()); - dds_log( + bpt_log( info, ".bold.cyan[NOTE:] Running applications may need to be restarted to see this change"_styled); } - auto fish_config = dds::user_config_dir() / "fish/config.fish"; + auto fish_config = bpt::user_config_dir() / "fish/config.fish"; if (fs::exists(fish_config)) { - auto fish_config_content = slurp_file(fish_config); - if (dds::contains(fish_config_content, "$HOME/.local/bin")) { + auto fish_config_content = bpt::read_file(fish_config); + if (bpt::contains(fish_config_content, "$HOME/.local/bin")) { // Assume that this is up-to-date - dds_log(info, + bpt_log(info, "Fish configuration in [.br.cyan[{}]] is okay"_styled, fish_config.string()); } else if (opts.dry_run) { - dds_log(info, + bpt_log(info, "Would update [.br.cyan[{}]] to have ~/.local/bin on $PATH"_styled, fish_config.string()); } else { - dds_log( + bpt_log( info, "Updating Fish shell configuration [.br.cyan[{}]] with user-local binaries PATH entry"_styled, fish_config.string()); fish_config_content - += ("\n# This line was added by 'dds install-yourself' to add the user-local " + += ("\n# This line was added by 'bpt install-yourself' to add the user-local " "binaries directory to $PATH\nset -x PATH $PATH \"$HOME/.local/bin\"\n"); auto tmp_file = fish_config; auto bak_file = fish_config; @@ -256,21 +261,21 @@ void fixup_user_path(const options& opts) { bak_file += ".bak"; neo_defer { if (!fs::exists(fish_config)) { - safe_rename(bak_file, fish_config); + move_file(bak_file, fish_config).value(); } }; // Write the temporary version - dds::write_file(tmp_file, fish_config_content).value(); + bpt::write_file(tmp_file, fish_config_content); // Make a backup - safe_rename(fish_config, bak_file); + move_file(fish_config, bak_file).value(); // Move the temp over the destination - safe_rename(tmp_file, fish_config); + move_file(tmp_file, fish_config).value(); // Okay! - dds_log(info, + bpt_log(info, "[.br.green[{}]] was updated. Prior contents are safe in [.br.cyan[{}]]"_styled, fish_config.string(), bak_file.string()); - dds_log( + bpt_log( info, ".bold.cyan[NOTE:] Running Fish shells will need to be restartred to see this change"_styled); } @@ -303,13 +308,13 @@ int _install_yourself(const options& opts) { ? user_binaries_dir() : system_binaries_dir(); - auto dest_path = dest_dir / "dds"; + auto dest_path = dest_dir / "bpt"; if constexpr (neo::os_is_windows) { dest_path += ".exe"; } if (fs::absolute(dest_path).lexically_normal() == fs::canonical(self_exe)) { - dds_log(error, + bpt_log(error, "We cannot install over our own executable (.br.red[{}])"_styled, self_exe.string()); return 1; @@ -317,38 +322,38 @@ int _install_yourself(const options& opts) { if (!fs::is_directory(dest_dir)) { if (opts.dry_run) { - dds_log(info, "Would create directory [.br.cyan[{}]]"_styled, dest_dir.string()); + bpt_log(info, "Would create directory [.br.cyan[{}]]"_styled, dest_dir.string()); } else { - dds_log(info, "Creating directory [.br.cyan[{}]]"_styled, dest_dir.string()); + bpt_log(info, "Creating directory [.br.cyan[{}]]"_styled, dest_dir.string()); fs::create_directories(dest_dir); } } if (opts.dry_run) { if (fs::is_symlink(dest_path)) { - dds_log(info, "Would remove symlink [.br.cyan[{}]]"_styled, dest_path.string()); + bpt_log(info, "Would remove symlink [.br.cyan[{}]]"_styled, dest_path.string()); } if (fs::exists(dest_path) && !fs::is_symlink(dest_path)) { if (opts.install_yourself.symlink) { - dds_log( + bpt_log( info, "Would overwrite .br.yellow[{0}] with a symlink .br.green[{0}] -> .br.cyan[{1}]"_styled, dest_path.string(), self_exe.string()); } else { - dds_log(info, + bpt_log(info, "Would overwrite .br.yellow[{}] with [.br.cyan[{}]]"_styled, dest_path.string(), self_exe.string()); } } else { if (opts.install_yourself.symlink) { - dds_log(info, + bpt_log(info, "Would create a symlink [.br.green[{}]] -> [.br.cyan[{}]]"_styled, dest_path.string(), self_exe.string()); } else { - dds_log(info, + bpt_log(info, "Would install [.br.cyan[{}]] to .br.yellow[{}]"_styled, self_exe.string(), dest_path.string()); @@ -356,25 +361,25 @@ int _install_yourself(const options& opts) { } } else { if (fs::is_symlink(dest_path)) { - dds_log(info, "Removing old symlink file [.br.cyan[{}]]"_styled, dest_path.string()); - dds::remove_file(dest_path).value(); + bpt_log(info, "Removing old symlink file [.br.cyan[{}]]"_styled, dest_path.string()); + bpt::remove_file(dest_path).value(); } if (opts.install_yourself.symlink) { if (fs::exists(dest_path)) { - dds_log(info, "Removing previous file [.br.cyan[{}]]"_styled, dest_path.string()); - dds::remove_file(dest_path).value(); + bpt_log(info, "Removing previous file [.br.cyan[{}]]"_styled, dest_path.string()); + bpt::remove_file(dest_path).value(); } - dds_log(info, + bpt_log(info, "Creating symbolic link [.br.green[{}]] -> [.br.cyan[{}]]"_styled, dest_path.string(), self_exe.string()); - dds::create_symlink(self_exe, dest_path).value(); + bpt::create_symlink(self_exe, dest_path).value(); } else { - dds_log(info, + bpt_log(info, "Installing [.br.cyan[{}]] to [.br.green[{}]]"_styled, self_exe.string(), dest_path.string()); - dds::copy_file(self_exe, dest_path, fs::copy_options::overwrite_existing).value(); + bpt::copy_file(self_exe, dest_path, fs::copy_options::overwrite_existing).value(); } } @@ -383,7 +388,7 @@ int _install_yourself(const options& opts) { } if (!opts.dry_run) { - dds_log(info, "Success!"); + bpt_log(info, "Success!"); } return 0; } @@ -392,15 +397,9 @@ int _install_yourself(const options& opts) { int install_yourself(const options& opts) { return boost::leaf::try_catch( - [&] { - try { - return _install_yourself(opts); - } catch (...) { - capture_exception(); - } - }, + [&] { return _install_yourself(opts); }, [](std::error_code ec, e_copy_file copy) { - dds_log(error, + bpt_log(error, "Failed to copy file [.br.cyan[{}]] to .br.yellow[{}]: .bold.red[{}]"_styled, copy.source.string(), copy.dest.string(), @@ -408,14 +407,14 @@ int install_yourself(const options& opts) { return 1; }, [](std::error_code ec, e_remove_file file) { - dds_log(error, + bpt_log(error, "Failed to delete file .br.yellow[{}]: .bold.red[{}]"_styled, file.value.string(), ec.message()); return 1; }, [](std::error_code ec, e_symlink oper) { - dds_log( + bpt_log( error, "Failed to create symlink from .br.yellow[{}] to [.br.cyan[{}]]: .bold.red[{}]"_styled, oper.symlink.string(), @@ -423,11 +422,11 @@ int install_yourself(const options& opts) { ec.message()); return 1; }, - [](e_system_error_exc e) { - dds_log(error, "Failure while installing: {}", e.message); + [](const std::system_error& e) { + bpt_log(error, "Failure while installing: {}", e.code().message()); return 1; }); return 0; } -} // namespace dds::cli::cmd +} // namespace bpt::cli::cmd diff --git a/src/bpt/cli/cmd/new.cpp b/src/bpt/cli/cmd/new.cpp new file mode 100644 index 00000000..6d842e28 --- /dev/null +++ b/src/bpt/cli/cmd/new.cpp @@ -0,0 +1,195 @@ +#include "../options.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +using namespace bpt; +using namespace fansi::literals; + +namespace bpt::cli::cmd { + +namespace { + +std::string get_argument(std::string_view prompt, + std::optional const& opt, + std::string_view default_) { + if (opt.has_value()) { + return opt.value(); + } + if (default_.empty()) { + fmt::print(".magenta[{}]: "_styled, prompt, default_); + } else { + fmt::print(".magenta[{}] [.blue[{}]]: "_styled, prompt, default_); + } + std::string ret; + std::getline(std::cin, ret); + if (!std::cin.good()) { + throw bpt::user_cancelled{}; + } + if (ret.empty()) { + return std::string(default_); + } + return ret; +} + +std::string to_ident(std::string_view given) { + std::string ret; + std::ranges::replace_copy_if(given, std::back_inserter(ret), BPT_TL(!std::isalnum(_1)), '_'); + if (!ret.empty() && std::isdigit(ret.front())) { + ret.insert(ret.begin(), '_'); + } + return ret; +} + +const std::string_view BPT_YAML_TEMPLATE = R"(# Project '{0}' created by 'bpt new' +name: {0} +version: 0.1.0 + +## Use 'dependencies' to declare dependencies on external libraries +# dependencies: +# - fmt@8.1.1 + +## Use 'test-dependencies' to declare dependencies on external libraries to be +## used by this project's tests +# test-dependencies: +# - catch2@2.13.9 using main + +## The relative filepath to the project's README file +readme: README.md + +## Provide a very brief description of the project +description: | + The is the {0} project. Hello! + +## Point to the project's homepage on the web +# homepage: example.com + +## The source-control repository of the project +# repository: example.com/{0}/source + +## A URL to the documentation for the project +# documentation: example.com/{0}/docs + +## Specify an SPX License ID for the project +# license: + +## List the authors of the project +# authors: +# - My Name +# - Other Name +)"; + +const std::string_view README_MD_TEMPLATE = R"( +# `{0}` - A Great New Project + +Add introductory information about your project to this file, which will likely +be the first thing that potential new users will see. +)"; + +} // namespace + +int new_cmd(const options& opts) { + auto given_name = opts.new_.name; + std::string name; + while (name.empty()) { + bpt_leaf_try { + name = bpt::name::from_string(get_argument("New project name", given_name, ""))->str; + } + bpt_leaf_catch(bpt::e_name_str given, bpt::invalid_name_reason why) { + given_name.reset(); + fmt::print(std::cerr, + "Invalid name string \".bold.red[{}]\": .bold.yellow[{}]\n"_styled, + given.value, + bpt::invalid_name_reason_str(why)); + }; + } + + std::string dest; + while (dest.empty()) { + auto default_dir = bpt::resolve_path_weak(fs::absolute(name)); + dest = get_argument("Project directory", opts.new_.directory, default_dir.string()); + if (dest.empty()) { + continue; + } + dest = bpt::resolve_path_weak(fs::absolute(dest)).string(); + if (fs::exists(dest)) { + if (!fs::is_directory(dest)) { + fmt::print(std::cerr, + "Path [.bold.red[{}]] names an existing non-directory file\n"_styled, + dest); + dest.clear(); + continue; + } + if (fs::directory_iterator{dest} != fs::directory_iterator{}) { + fmt::print(std::cerr, + "Path [.bold.red[{}]] is an existing non-empty directory\n"_styled, + dest); + dest.clear(); + continue; + } + } + } + + bool split_dir = false; + if (opts.new_.split_src_include.has_value()) { + split_dir = *opts.new_.split_src_include; + } else { + while (true) { + auto got = get_argument( + "Split headers and sources into [include/] and [src/] directories? [y/N]", + std::nullopt, + ""); + if (got == neo::oper::any_of("n", "N", "no", "")) { + split_dir = false; + break; + } else if (got == neo::oper::any_of("y", "Y", "yes")) { + split_dir = true; + break; + } else { + fmt::print(std::cerr, "Enter one of '.bold.cyan[y]' or '.bold.cyan[n]'"_styled); + } + } + } + + fs::path project_dir = dest; + fs::create_directories(project_dir); + bpt::write_file(project_dir / "bpt.yaml", fmt::format(BPT_YAML_TEMPLATE, name)); + + fs::path header_dir = split_dir ? (project_dir / "include") : (project_dir / "src"); + fs::path src_dir = project_dir / "src"; + fs::create_directories(header_dir / name); + fs::create_directories(src_dir / name); + bpt::write_file(src_dir / name / fmt::format("{}.cpp", name), + fmt::format("#include <{0}/{0}.hpp>\n\n" + "int {1}::the_answer() noexcept {{\n" + " return 42;\n" + "}}\n", + name, + to_ident(name))); + + bpt::write_file(header_dir / name / fmt::format("{}.hpp", name), + fmt::format("#pragma once\n\n" + "namespace {0} {{\n\n" + "// Calculate the answer\n" + "int the_answer() noexcept;\n\n" + "}}\n", + to_ident(name))); + + bpt::write_file(project_dir / "README.md", fmt::format(README_MD_TEMPLATE, name)); + fmt::print("New project files written to [.bold.cyan[{}]]\n"_styled, dest); + return 0; +} + +} // namespace bpt::cli::cmd diff --git a/src/bpt/cli/cmd/pkg_create.cpp b/src/bpt/cli/cmd/pkg_create.cpp new file mode 100644 index 00000000..a5be19b9 --- /dev/null +++ b/src/bpt/cli/cmd/pkg_create.cpp @@ -0,0 +1,98 @@ +#include "../options.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace fansi::literals; + +namespace bpt::cli::cmd { + +int pkg_create(const options& opts) { + bpt::sdist_params params{ + .project_dir = opts.absolute_project_dir_path(), + .dest_path = {}, + .force = opts.if_exists == if_exists::replace, + .revision = opts.pkg.create.revision, + }; + if (params.revision < 1) { + bpt_log( + error, + ".bold.cyan[--revision] must be greater than or equal to one '1' (Got .bold.red[{}])"_styled, + params.revision); + return 1; + } + return bpt_leaf_try { + auto sd = sdist::from_directory(params.project_dir); + sd.pkg.id.revision = params.revision; + auto default_filename = fmt::format("{}.tar.gz", sd.pkg.id.to_string()); + auto filepath = opts.out_path.value_or(fs::current_path() / default_filename); + create_sdist_targz(filepath, params); + bpt_log(info, + "Created source distribution archive: .bold.cyan[{}]"_styled, + filepath.string()); + return 0; + } + bpt_leaf_catch(e_sdist_from_directory dirpath, + e_missing_pkg_json expect_pkg_json, + e_missing_project_yaml expect_proj_json5) { + bpt_log( + error, + "No package or project files are presenting in the directory [.bold.red[{}]]"_styled, + dirpath.value.string()); + bpt_log(error, "Expected [.bold.yellow[{}]]"_styled, expect_pkg_json.value.string()); + bpt_log(error, " or [.bold.yellow[{}]]"_styled, expect_proj_json5.value.string()); + write_error_marker("no-pkg-meta-files"); + return 1; + } + bpt_leaf_catch(e_sdist_from_directory, + e_json_parse_error error, + const std::filesystem::path& fpath) { + bpt_log(error, + "Invalid JSON/JSON5 file [.bold.yellow[{}]]: .bold.red[{}]"_styled, + fpath.string(), + error.value); + write_error_marker("package-json5-parse-error"); + return 1; + } + bpt_leaf_catch(const std::system_error& exc, + e_sdist_from_directory dirpath, + std::filesystem::path const* fpath) { + bpt_log(error, + "Error while opening source distribution from [{}]: {}"_styled, + dirpath.value.string(), + exc.what()); + if (fpath) { + bpt_log(error, " (Failing path was [.bold.yellow[{}]])"_styled, fpath->string()); + } + write_error_marker("sdist-open-fail-generic"); + return 1; + } + bpt_leaf_catch(boost::leaf::catch_> exc)->int { + if (opts.if_exists == if_exists::ignore) { + // Satisfy the 'ignore' semantics by returning a success exit code, but still warn + // the user to let them know what happened. + bpt_log(warn, "{}", exc.matched.what()); + return 0; + } + // If if_exists::replace, we wouldn't be here (not an error). Thus, since it's not + // if_exists::ignore, it must be if_exists::fail here. + neo_assert_always(invariant, + opts.if_exists == if_exists::fail, + "Unhandled value for if_exists"); + + write_error_marker("sdist-already-exists"); + // rethrow; the default handling works. + throw; + }; +} + +} // namespace bpt::cli::cmd diff --git a/src/bpt/cli/cmd/pkg_prefetch.cpp b/src/bpt/cli/cmd/pkg_prefetch.cpp new file mode 100644 index 00000000..13bcab1b --- /dev/null +++ b/src/bpt/cli/cmd/pkg_prefetch.cpp @@ -0,0 +1,22 @@ +#include "../options.hpp" + +#include "./cache_util.hpp" + +#include +#include +#include +#include +#include + +namespace bpt::cli::cmd { + +int pkg_prefetch(const options& opts) { + auto cache = open_ready_cache(opts); + for (auto& pkg_str : opts.pkg.prefetch.pkgs) { + auto pid = crs::pkg_id::parse(pkg_str); + cache.prefetch(pid); + } + return 0; +} + +} // namespace bpt::cli::cmd diff --git a/src/bpt/cli/cmd/pkg_search.cpp b/src/bpt/cli/cmd/pkg_search.cpp new file mode 100644 index 00000000..1fbc2b1c --- /dev/null +++ b/src/bpt/cli/cmd/pkg_search.cpp @@ -0,0 +1,116 @@ +#include "../options.hpp" + +#include "./cache_util.hpp" +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +using namespace fansi::literals; +namespace nsql = neo::sqlite3; + +namespace { + +struct pkg_group_search_result { + std::string name; + std::vector versions; + std::string remote_url; +}; + +struct pkg_search_results { + std::vector found; +}; + +pkg_search_results search_impl(nsql::connection_ref db, std::optional pattern) { + auto search_st = *db.prepare(R"( + SELECT pkg.name, + group_concat(version, ';;'), + remote.url + FROM bpt_crs_packages AS pkg + JOIN bpt_crs_remotes AS remote USING(remote_id) + WHERE lower(pkg.name) GLOB lower(:pattern) + AND remote_id IN (SELECT remote_id FROM bpt_crs_enabled_remotes) + GROUP BY pkg.name, remote_id + ORDER BY remote.unique_name, pkg.name + )"); + // If no pattern, grab _everything_ + auto final_pattern = pattern.value_or("*"); + bpt_log(debug, "Searching for packages matching pattern '{}'", final_pattern); + search_st.bindings()[1] = final_pattern; + auto rows = nsql::iter_tuples(search_st); + + std::vector found; + for (auto [name, versions, remote_url] : rows) { + bpt_log(debug, "Found: {} with versions {} [{}]", name, versions, remote_url); + auto version_strs = bpt::split(versions, ";;"); + auto versions_semver + = version_strs | std::views::transform(&semver::version::parse) | neo::to_vector; + std::ranges::sort(versions_semver); + found.push_back(pkg_group_search_result{ + .name = name, + .versions = versions_semver, + .remote_url = remote_url, + }); + } + + if (found.empty()) { + BOOST_LEAF_THROW_EXCEPTION([&] { + auto names_st = *db.prepare(R"( + SELECT DISTINCT name from bpt_crs_packages + WHERE remote_id IN (SELECT remote_id FROM bpt_crs_enabled_remotes) + )"); + auto tups = nsql::iter_tuples(names_st); + auto names_vec = tups + | std::views::transform([](auto&& row) { return row.template get<0>(); }) + | neo::to_vector; + auto nearest = bpt::did_you_mean(final_pattern, names_vec); + return bpt::e_nonesuch{final_pattern, nearest}; + }); + } + + return pkg_search_results{.found = std::move(found)}; +} +} // namespace + +namespace bpt::cli::cmd { + +static int _pkg_search(const options& opts) { + auto cache = open_ready_cache(opts); + + auto results = search_impl(cache.db().sqlite3_db(), opts.pkg.search.pattern); + for (pkg_group_search_result const& found : results.found) { + fmt::print( + " Name: .bold[{}]\n" + "Versions: .bold[{}]\n" + " From: .bold[{}]\n\n"_styled, + found.name, + joinstr(", ", found.versions | std::views::transform(&semver::version::to_string)), + found.remote_url); + } + + return 0; +} + +int pkg_search(const options& opts) { + return boost::leaf::try_catch( + [&] { return _pkg_search(opts); }, + [](e_nonesuch missing) { + missing.log_error( + "There are no packages that match the given pattern \".bold.red[{}]\""_styled); + write_error_marker("pkg-search-no-result"); + return 1; + }); +} + +} // namespace bpt::cli::cmd diff --git a/src/bpt/cli/cmd/pkg_solve.cpp b/src/bpt/cli/cmd/pkg_solve.cpp new file mode 100644 index 00000000..439a6835 --- /dev/null +++ b/src/bpt/cli/cmd/pkg_solve.cpp @@ -0,0 +1,74 @@ +#include "../options.hpp" + +#include "./cache_util.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +using namespace fansi::literals; + +namespace bpt::cli::cmd { + +static int _pkg_solve(const options& opts) { + auto cache = open_ready_cache(opts); + + auto deps = // + opts.pkg.solve.reqs // + | std::views::transform([](std::string_view s) -> crs::dependency { + return project_dependency::from_shorthand_string(s).as_crs_dependency(); + }); + + auto sln = bpt::solve(cache.db(), deps); + for (auto&& pkg : sln) { + bpt_log(info, "Require: {}", pkg.to_string()); + } + return 0; +} + +int pkg_solve(const options& opts) { + return bpt_leaf_try { return _pkg_solve(opts); } + bpt_leaf_catch(e_dependency_solve_failure, + e_dependency_solve_failure_explanation explain, + const std::vector* missing_pkgs, + const std::vector* missing_libs) { + bpt_log(error, + "No solution is possible with the known package information: \n{}"_styled, + explain.value); + if (missing_pkgs) { + for (auto& missing : *missing_pkgs) { + missing.log_error( + "Direct requirement on '.bold.red[{}]' does not name an existing package in any enabled repositories"_styled); + } + } + if (missing_libs) { + for (auto& lib : *missing_libs) { + bpt_log( + error, + "There is no available version of .bold.yellow[{}] that contains a library named \".bold.red[{}]\""_styled, + lib.pkg_name.str, + lib.lib.given); + if (lib.lib.nearest) { + bpt_log(error, + " (Did you mean \".bold.yellow[{}]\"?)"_styled, + *lib.lib.nearest); + } + } + } + write_error_marker("no-dependency-solution"); + return 1; + }; +} + +} // namespace bpt::cli::cmd diff --git a/src/bpt/cli/cmd/repo_cmd.cpp b/src/bpt/cli/cmd/repo_cmd.cpp new file mode 100644 index 00000000..2e54096f --- /dev/null +++ b/src/bpt/cli/cmd/repo_cmd.cpp @@ -0,0 +1,71 @@ +#include "../options.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace bpt; +using namespace fansi::literals; + +namespace bpt::cli::cmd { + +using command = int(const options&); + +command repo_init; +command repo_import; +command repo_ls; +command repo_validate; +command repo_remove; + +int repo_cmd(const options& opts) { + neo_assert(invariant, opts.subcommand == subcommand::repo, "Wrong subcommand for dispatch"); + return bpt_leaf_try { + switch (opts.repo.subcommand) { + case repo_subcommand::init: + return cmd::repo_init(opts); + case repo_subcommand::import: + return cmd::repo_import(opts); + case repo_subcommand::ls: + return cmd::repo_ls(opts); + case repo_subcommand::validate: + return cmd::repo_validate(opts); + case repo_subcommand::remove: + return cmd::repo_remove(opts); + case repo_subcommand::_none_:; + } + neo::unreachable(); + } + bpt_leaf_catch(bpt::crs::e_repo_open_path db_path, matchv) { + bpt_log( + error, + "Repository [.br.yellow[{}]] is from a newer bpt version. We don't know how to handle it."_styled, + db_path.value.string()); + write_error_marker("repo-db-too-new"); + return 1; + } + bpt_leaf_catch(bpt::crs::e_repo_open_path repo_path, e_migration_error error) { + bpt_log( + error, + "Error while applying database migrations when opening SQLite database for repostiory [.br.yellow[{}]]: .br.red[{}]"_styled, + repo_path.value.string(), + error.value); + write_error_marker("repo-db-invalid"); + return 1; + } + bpt_leaf_catch(bpt::crs::e_repo_open_path, bpt::e_db_open_path db_path, bpt::e_db_open_ec ec) { + bpt_log(error, + "Error opening repository database [.br.yellow[{}]]: {}"_styled, + db_path.value, + ec.value.message()); + write_error_marker("repo-repo-open-fails"); + return 1; + }; +} + +} // namespace bpt::cli::cmd \ No newline at end of file diff --git a/src/bpt/cli/cmd/repo_import.cpp b/src/bpt/cli/cmd/repo_import.cpp new file mode 100644 index 00000000..3c3ed5e9 --- /dev/null +++ b/src/bpt/cli/cmd/repo_import.cpp @@ -0,0 +1,172 @@ +#include "../options.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace fansi::literals; + +namespace bpt::cli::cmd { + +namespace { + +void _import_file(bpt::crs::repository& repo, path_ref dirpath) { repo.import_dir(dirpath); } + +int _repo_import(const options& opts) { + auto repo = bpt::crs::repository::open_existing(opts.repo.repo_dir); + NEO_SUBSCRIBE(bpt::crs::ev_repo_imported_package imported) { + bpt_log(info, + "[{}]: Imported .bold.cyan[{}] from [.br.cyan[{}]]"_styled, + imported.into_repo.name(), + imported.pkg_meta.id.to_string(), + imported.from_path.string()); + }; + for (auto& path : opts.repo.import.files) { + bpt_leaf_try { + bpt_log(debug, "Importing CRS package from [.br.cyan[{}]]"_styled, path.string()); + _import_file(repo, path); + } + bpt_leaf_catch(bpt::crs::e_repo_import_pkg_already_present, + bpt::crs::e_repo_importing_package meta) { + switch (opts.if_exists) { + case if_exists::ignore: + bpt_log(info, + "Ignoring existing package .br.cyan[{}] (from .br.white[{}])"_styled, + meta.value.id.to_string(), + path.string()); + return; + case if_exists::fail: + throw; + case if_exists::replace: + bpt_log(info, + "Replacing existing package .br.yellow[{}]"_styled, + meta.value.id.to_string()); + repo.remove_pkg(meta.value); + _import_file(repo, path); + return; + } + }; + } + return 0; +} + +} // namespace + +int repo_import(const options& opts) { + return bpt_leaf_try { return _repo_import(opts); } + bpt_leaf_catch(bpt::crs::e_repo_import_pkg_already_present, + bpt::crs::e_repo_importing_package meta, + bpt::crs::e_repo_importing_dir from_dir) { + bpt_log( + error, + "Refusing to overwrite existing package .br.yellow[{}] (Importing from [.br.yellow[{}]])"_styled, + meta.value.id.to_string(), + from_dir.value.string()); + write_error_marker("repo-import-pkg-already-exists"); + return 1; + } + bpt_leaf_catch(bpt::crs::e_repo_importing_dir crs_dir, + std::error_code ec, + boost::leaf::match>, + e_sqlite3_error const* sqlite_error) { + bpt_log(error, + "SQLite error while importing [.br.yellow[{}]]"_styled, + crs_dir.value.string()); + bpt_log(error, " .br.red[{}]"_styled, ec.message()); + if (sqlite_error) { + bpt_log(error, " .br.red[{}]"_styled, sqlite_error->value); + } + bpt_log(error, "(It's possible that the database is invalid or corrupted)"); + write_error_marker("repo-import-db-error"); + return 1; + } + bpt_leaf_catch(bpt::e_json_parse_error parse_error, + bpt::crs::e_repo_importing_dir crs_dir, + bpt::crs::e_pkg_json_path pkg_json_path) { + bpt_log(error, "Error while importing [.br.yellow[{}]]"_styled, crs_dir.value.string()); + bpt_log(error, + " JSON parse error in [.br.yellow[{}]]:"_styled, + pkg_json_path.value.string()); + bpt_log(error, " .br.red[{}]"_styled, parse_error.value); + write_error_marker("repo-import-invalid-crs-json-parse-error"); + return 1; + } + bpt_leaf_catch(bpt::crs::e_invalid_meta_data error, + bpt::crs::e_repo_importing_dir crs_dir, + bpt::crs::e_pkg_json_path pkg_json_path) { + bpt_log(error, "Error while importing [.br.yellow[{}]]"_styled, crs_dir.value.string()); + bpt_log(error, + "CRS data in [.br.yellow[{}]] is invalid: .br.red[{}]"_styled, + pkg_json_path.value.string(), + error.value); + write_error_marker("repo-import-invalid-crs-json"); + return 1; + } + bpt_leaf_catch(bpt::crs::e_invalid_meta_data error, + bpt::crs::e_repo_importing_dir proj_dir, + bpt::e_open_project, + bpt::e_parse_project_manifest_path proj_json_path) { + bpt_log(error, "Error while importing [.br.yellow[{}]]"_styled, proj_dir.value.string()); + bpt_log(error, + "Project data in [.br.yellow[{}]] is invalid: .br.red[{}]"_styled, + proj_json_path.value.string(), + error.value); + write_error_marker("repo-import-invalid-proj-json"); + return 1; + } + bpt_leaf_catch(user_error const& exc, + crs::e_repo_importing_dir crs_dir) { + bpt_log(error, + "Error while importing [.br.yellow[{}]]: .br.red[{}]"_styled, + crs_dir.value.string(), + exc.what()); + write_error_marker("repo-import-noent"); + return 1; + } + bpt_leaf_catch(bpt::crs::e_repo_importing_dir crs_dir, + bpt::e_read_file_path const* read_file, + bpt::e_copy_file const* copy_file, + std::error_code ec) { + bpt_log(error, "Error while importing [.br.red[{}]]:"_styled, crs_dir.value.string()); + if (read_file) { + bpt_log(error, + " Error reading file [.br.yellow[{}]]:"_styled, + read_file->value.string(), + ec.message()); + } else if (copy_file) { + bpt_log(error, + " Error copying file [.br.yellow[{}]] to [.br.yellow[{}]]:"_styled, + copy_file->source.string(), + copy_file->dest.string()); + } + bpt_log(error, " .br.red[{}]"_styled, ec.message()); + write_error_marker("repo-import-noent"); + return 1; + } + bpt_leaf_catch(crs::e_repo_importing_dir crs_dir, + crs::e_repo_importing_package meta, + crs::e_repo_import_invalid_pkg_version err) { + bpt_log(info, + "Error while importing .br.yellow[{}] (from [.br.yellow[{}]]):"_styled, + meta.value.id.to_string(), + crs_dir.value.string()); + bpt_log(error, "Invalid 'pkg-version' on package: .br.red[{}]"_styled, err.value); + write_error_marker("repo-import-invalid-pkg-version"); + return 1; + }; +} + +} // namespace bpt::cli::cmd diff --git a/src/bpt/cli/cmd/repo_init.cpp b/src/bpt/cli/cmd/repo_init.cpp new file mode 100644 index 00000000..cdbaa198 --- /dev/null +++ b/src/bpt/cli/cmd/repo_init.cpp @@ -0,0 +1,62 @@ +#include "../options.hpp" + +#include +#include +#include +#include +#include + +namespace bpt::cli::cmd { + +namespace { + +int _repo_init(const options& opts) { + auto try_create = [&] { + auto repo = bpt::crs::repository::create(opts.repo.repo_dir, opts.repo.init.name); + bpt_log(info, + "Created a new CRS repository '{}' in [{}]", + repo.name(), + repo.root().string()); + return 0; + }; + return bpt_leaf_try { return try_create(); } + bpt_leaf_catch(bpt::crs::e_repo_already_init) { + switch (opts.if_exists) { + case if_exists::ignore: + bpt_log(info, + "Directory [{}] already contains a CRS repository", + opts.repo.repo_dir.string()); + return 0; + case if_exists::replace: + bpt_log(info, + "Removing existing repository database [{}]", + opts.repo.repo_dir.string()); + bpt::ensure_absent(opts.repo.repo_dir / "repo.db").value(); + return try_create(); + case if_exists::fail: + throw; + } + neo::unreachable(); + }; +} +} // namespace + +int repo_init(const options& opts) { + return bpt_leaf_try { return _repo_init(opts); } + bpt_leaf_catch(bpt::crs::e_repo_already_init, bpt::crs::e_repo_open_path dirpath) { + bpt_log(error, + "Failed to initialize a new repostiory at [{}]: Repository already exists", + dirpath.value.string()); + write_error_marker("repo-init-already-init"); + return 1; + } + bpt_leaf_catch(std::error_code ec, bpt::crs::e_repo_open_path dirpath) { + bpt_log(error, + "Error while initializing new repository at [{}]: {}", + dirpath.value.string(), + ec.message()); + return 1; + }; +} + +} // namespace bpt::cli::cmd diff --git a/src/bpt/cli/cmd/repo_ls.cpp b/src/bpt/cli/cmd/repo_ls.cpp new file mode 100644 index 00000000..66f6dd72 --- /dev/null +++ b/src/bpt/cli/cmd/repo_ls.cpp @@ -0,0 +1,23 @@ +#include "../options.hpp" + +#include +#include +#include + +#include +#include + +#include + +namespace bpt::cli::cmd { + +int repo_ls(const options& opts) { + auto repo = bpt::crs::repository::open_existing(opts.repo.repo_dir); + for (auto pkg : repo.all_packages()) { + fmt::print(std::cout, pkg.id.to_string()); + std::cout << '\n'; + } + return 0; +} + +} // namespace bpt::cli::cmd diff --git a/src/bpt/cli/cmd/repo_remove.cpp b/src/bpt/cli/cmd/repo_remove.cpp new file mode 100644 index 00000000..dfe2459b --- /dev/null +++ b/src/bpt/cli/cmd/repo_remove.cpp @@ -0,0 +1,44 @@ +#include "../options.hpp" + +#include +#include +#include +#include +#include +#include // for e_nonesuch_package + +#include +#include +#include + +#include + +using namespace fansi::styled_literals; + +namespace bpt::cli::cmd { + +int repo_remove(const options& opts) { + auto repo = bpt::crs::repository::open_existing(opts.repo.repo_dir); + for (auto pkg : opts.repo.remove.pkgs) { + auto pkg_id = bpt::crs::pkg_id::parse(pkg); + bpt_log(info, "Removing .bold.yellow[{}]"_styled, pkg_id.to_string()); + /// We only need the name and version info to do the removal + bpt::crs::package_info meta; + meta.id = pkg_id; + bpt_leaf_try { repo.remove_pkg(meta); } + bpt_leaf_catch(bpt::e_nonesuch_package missing) { + if (opts.if_missing == if_missing::ignore) { + bpt_log(info, + "Ignoring non-existent package .bold.yellow[{}]"_styled, + missing.given); + } else { + missing.log_error("No such package .bold.red[{}]"_styled); + write_error_marker("pkg-remove-nonesuch"); + bpt::throw_system_exit(1); + } + }; + } + return 0; +} + +} // namespace bpt::cli::cmd diff --git a/src/bpt/cli/cmd/repo_validate.cpp b/src/bpt/cli/cmd/repo_validate.cpp new file mode 100644 index 00000000..02cebaef --- /dev/null +++ b/src/bpt/cli/cmd/repo_validate.cpp @@ -0,0 +1,115 @@ +#include "../options.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace neo::sqlite3::literals; +using namespace fansi::literals; + +namespace bpt::cli::cmd { + +static bool try_it(const crs::package_info& pkg, crs::cache_db& cache) { + auto dep = {crs::dependency{ + .name = pkg.id.name, + .acceptable_versions = crs::version_range_set{pkg.id.version, pkg.id.version.next_after()}, + .uses = pkg.libraries | std::views::transform(BPT_TL(_1.name)) | neo::to_vector, + }}; + return bpt_leaf_try { + fmt::print("Validate package .br.cyan[{}] ..."_styled, pkg.id.to_string()); + std::cout.flush(); + neo_defer { fmt::print("\r\x1b[K"); }; + bpt::solve(cache, dep); + return true; + } + bpt_leaf_catch(e_usage_no_such_lib, + lm::usage bad_usage, + crs::dependency, + crs::package_info dep_pkg) { + bpt_log(error, "Package .bold.red[{}] is not valid:"_styled, pkg.id.to_string()); + bpt_log( + error, + "It requests a usage of library .br.red[{}] from .br.yellow[{}], which does not exist in that package."_styled, + bad_usage, + dep_pkg.id.to_string()); + auto yellows + = dep_pkg.libraries | std::views::transform([&](auto&& _1) { + return fmt::format(".yellow[{}/{}]"_styled, dep_pkg.id.name.str, _1.name.str); + }); + bpt_log(error, + " (.br.yellow[{}] defines {})"_styled, + dep_pkg.id.to_string(), + joinstr(", ", yellows)); + return false; + } + bpt_leaf_catch(e_dependency_solve_failure, e_dependency_solve_failure_explanation explain) { + bpt_log( + error, + "Installation of .bold.red[{}] is not possible with the known package information: \n{}"_styled, + pkg.id.to_string(), + explain.value); + return false; + } + bpt_leaf_catch(bpt::user_cancelled)->bpt::noreturn_t { throw; } + bpt_leaf_catch_all { + bpt_log(error, "Package validation error: {}", diagnostic_info); + return false; + }; +} + +int repo_validate(const options& opts) { + auto repo = bpt::crs::repository::open_existing(opts.repo.repo_dir); + auto db = bpt::unique_database::open("").value(); + auto cache = bpt::crs::cache_db::open(db); + int n_errors = 0; + + for (auto& r : opts.use_repos) { + auto url = bpt::guess_url_from_string(r); + cache.sync_remote(url); + cache.enable_remote(url); + } + + auto fs_url = neo::url::for_file_path(repo.root()); + cache.sync_remote(fs_url); + cache.enable_remote(fs_url); + + // We only want to validate packages that are the max revision: + for (auto&& pkg : repo.all_latest_rev_packages()) { + bpt::cancellation_point(); + const bool okay = try_it(pkg, cache); + if (!okay) { + n_errors++; + } + } + + if (n_errors) { + if (n_errors == 1) { + bpt_log(error, ".bold.red[1 package] is not possibly installible"_styled); + } else { + bpt_log(error, ".bold.red[{} packages] are not possibly installible"_styled, n_errors); + } + write_error_marker("repo-invalid"); + return 1; + } + return 0; +} + +} // namespace bpt::cli::cmd diff --git a/src/bpt/cli/dispatch_main.cpp b/src/bpt/cli/dispatch_main.cpp new file mode 100644 index 00000000..b784cc5a --- /dev/null +++ b/src/bpt/cli/dispatch_main.cpp @@ -0,0 +1,69 @@ +#include "./dispatch_main.hpp" + +#include "./error_handler.hpp" +#include "./options.hpp" + +#include +#include + +using namespace bpt; + +namespace bpt::cli { + +namespace cmd { +using command = int(const options&); + +command build_deps; +command build; +command compile_file; +command install_yourself; +command pkg_create; +command pkg_search; +command pkg_prefetch; +command pkg_solve; +command repo_cmd; +command new_cmd; + +} // namespace cmd + +int dispatch_main(const options& opts) noexcept { + return bpt::handle_cli_errors([&] { + BPT_E_SCOPE(opts.subcommand); + switch (opts.subcommand) { + case subcommand::new_: + return cmd::new_cmd(opts); + case subcommand::build: + return cmd::build(opts); + case subcommand::pkg: { + BPT_E_SCOPE(opts.pkg.subcommand); + switch (opts.pkg.subcommand) { + case pkg_subcommand::create: + return cmd::pkg_create(opts); + case pkg_subcommand::search: + return cmd::pkg_search(opts); + case pkg_subcommand::prefetch: + return cmd::pkg_prefetch(opts); + case pkg_subcommand::solve: + return cmd::pkg_solve(opts); + case pkg_subcommand::_none_:; + } + neo::unreachable(); + } + case subcommand::repo: { + BPT_E_SCOPE(opts.repo.subcommand); + return cmd::repo_cmd(opts); + } + case subcommand::compile_file: + return cmd::compile_file(opts); + case subcommand::build_deps: + return cmd::build_deps(opts); + case subcommand::install_yourself: + return cmd::install_yourself(opts); + case subcommand::_none_:; + } + neo::unreachable(); + return 6; + }); +} + +} // namespace bpt::cli diff --git a/src/dds/cli/dispatch_main.hpp b/src/bpt/cli/dispatch_main.hpp similarity index 63% rename from src/dds/cli/dispatch_main.hpp rename to src/bpt/cli/dispatch_main.hpp index 3db6cbd8..06431735 100644 --- a/src/dds/cli/dispatch_main.hpp +++ b/src/bpt/cli/dispatch_main.hpp @@ -1,9 +1,9 @@ #pragma once -namespace dds::cli { +namespace bpt::cli { struct options; int dispatch_main(const options&) noexcept; -} // namespace dds::cli \ No newline at end of file +} // namespace bpt::cli \ No newline at end of file diff --git a/src/bpt/cli/error_handler.cpp b/src/bpt/cli/error_handler.cpp new file mode 100644 index 00000000..6668c8f2 --- /dev/null +++ b/src/bpt/cli/error_handler.cpp @@ -0,0 +1,317 @@ +#include "./error_handler.hpp" +#include "./options.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace bpt; +using namespace fansi::literals; + +using boost::leaf::catch_; + +namespace { + +auto handlers = std::tuple( // + [](e_sdist_from_directory sdist_dirpath, + e_json_parse_error error, + const std::filesystem::path* maybe_fpath) { + bpt_log( + error, + "Invalid metadata file while opening source distribution/project in [.bold.yellow[{}]]"_styled, + sdist_dirpath.value.string()); + bpt_log(error, "Invalid JSON file: .bold.red[{}]"_styled, error.value); + if (maybe_fpath) { + bpt_log(error, " (While reading from [{}])", maybe_fpath->string()); + } + write_error_marker("package-json5-parse-error"); + return 1; + }, + [](e_sdist_from_directory sdist_dirpath, + bpt::e_yaml_parse_error error, + const std::filesystem::path* maybe_fpath) { + bpt_log( + error, + "Invalid metadata file while opening source distribution/project in [.bold.yellow[{}]]"_styled, + sdist_dirpath.value.string()); + bpt_log(error, "Invalid YAML file: .bold.red[{}]"_styled, error.value); + if (maybe_fpath) { + bpt_log(error, " (While reading from [{}])", maybe_fpath->string()); + } + write_error_marker("package-yaml-parse-error"); + return 1; + }, + [](e_sdist_from_directory, e_parse_project_manifest_path bpt_yaml, e_bad_bpt_yaml_key badkey) { + bpt_log(error, + "Error loading project info from [.bold.yellow[{}]]"_styled, + bpt_yaml.value.string()); + badkey.log_error("Unknown project property '.bold.red[{}]'"_styled); + return 1; + }, + [](e_parse_dependency_manifest_path deps_json, e_bad_deps_json_key badkey) { + bpt_log(error, + "Error loading dependency info from [.bold.yellow[{}]]"_styled, + deps_json.value.string()); + badkey.log_error("Unknown property '.bold.red[{}]'"_styled); + return 1; + }, + [](const semester::walk_error& exc, + e_sdist_from_directory dir, + e_parse_project_manifest_path* bpt_yaml, + crs::e_pkg_json_path* pkg_json) { + if (pkg_json) { + bpt_log(error, + "Error loading package info from [.bold.yellow[{}]]: .bold.red[{}]"_styled, + pkg_json->value.string(), + exc.what()); + write_error_marker("invalid-pkg-json"); + } else if (bpt_yaml) { + bpt_log(error, + "Error loading project info from [.bold.yellow[{}]]: .bold.red[{}]"_styled, + bpt_yaml->value.string(), + exc.what()); + write_error_marker("invalid-pkg-yaml"); + } else { + bpt_log(error, + "Error parsing data in directory [.bold.yellow[{}]]: .bold.red[{}]"_styled, + dir.value.string(), + exc.what()); + } + return 1; + }, + [](const neo::url_validation_error& exc, + e_sdist_from_directory, + e_parse_project_manifest_path bpt_yaml, + e_url_string str) { + bpt_log( + error, + "Error while parsing URL string '.bold.yellow[{}]' in [.bold.yellow[{}]]: .bold.red[{}]"_styled, + str.value, + bpt_yaml.value.string(), + exc.what()); + return 1; + }, + [](bpt::e_bad_spdx_expression err, + bpt::e_spdx_license_str spdx_str, + e_sdist_from_directory, + e_parse_project_manifest_path bpt_yaml) { + bpt_log(error, + "Invalid SPDX license expression '.bold.yellow[{}]': .bold.red[{}]"_styled, + spdx_str.value, + err.value); + bpt_log(error, + " (While reading project manifest from [.bold.yellow[{}]]"_styled, + bpt_yaml.value.string()); + write_error_marker("invalid-spdx"); + return 1; + }, + [](user_cancelled) { + bpt_log(critical, "Operation cancelled by the user"); + return 2; + }, + [](const std::system_error& e, neo::url url, http_response_info) { + bpt_log(error, + "An error occurred while downloading [.bold.red[{}]]: {}"_styled, + url.to_string(), + e.code().message()); + return 1; + }, + [](const std::system_error& e, network_origin origin, neo::url* url) { + bpt_log(error, + "Network error communicating with .bold.red[{}://{}:{}]: {}"_styled, + origin.protocol, + origin.hostname, + origin.port, + e.code().message()); + if (url) { + bpt_log(error, " (While accessing URL [.bold.red[{}]])"_styled, url->to_string()); + } + return 1; + }, + [](const std::system_error& err, e_loading_toolchain, bpt::e_toolchain_filepath* tc_file) { + bpt_log(error, "Failed to load toolchain: .br.yellow[{}]"_styled, err.code().message()); + if (tc_file) { + bpt_log(error, " (While loading from file [.bold.red[{}]])"_styled, tc_file->value); + } + write_error_marker("bad-toolchain"); + return 1; + }, + [](e_bad_toolchain_key nonesuch, e_loading_toolchain, e_toolchain_filepath* tc_file) { + nonesuch.log_error("Unknown toolchain option: '.br.red[{}]'"_styled); + if (tc_file) { + bpt_log(error, + " (While reading toolchain from file [.bold.yellow[{}]])"_styled, + tc_file->value); + } + write_error_marker("bad-toolchain-opt"); + return 1; + }, + [](e_name_str badname, + invalid_name_reason why, + e_parse_dep_range_shorthand_string depstr, + e_parse_project_manifest_path const* prman_path) { + bpt_log( + error, + "Invalid package name '.bold.red[{}]' in dependency string '.br.red[{}]': .br.yellow[{}]"_styled, + badname.value, + depstr.value, + invalid_name_reason_str(why)); + if (prman_path) { + bpt_log(error, + " (While reading project manifest from [.bold.yellow[{}]])"_styled, + prman_path->value); + } + write_error_marker("invalid-pkg-dep-name"); + return 1; + }, + [](e_name_str badname, invalid_name_reason why, e_parse_project_manifest_path* prman_path) { + bpt_log(error, + "Invalid name string '.bold.red[{}]': .br.yellow[{}]"_styled, + badname.value, + invalid_name_reason_str(why)); + if (prman_path) { + bpt_log(error, + " (While reading project manifest from [.bold.yellow[{}]])"_styled, + prman_path->value); + } + write_error_marker("invalid-name"); + return 1; + }, + [](e_parse_dep_shorthand_string given, + e_parse_dep_range_shorthand_string const* range_part, + e_human_message msg, + e_parse_project_manifest_path const* proj_man) { + bpt_log(error, + "Invalid dependency shorthand string '.bold.yellow[{}]'"_styled, + given.value); + if (range_part) { + bpt_log(error, + " (While parsing name+range string '.bold.yellow[{}]')"_styled, + range_part->value); + } + if (proj_man) { + bpt_log(error, + " (While reading project manifest from [.bold.yellow[{}]]"_styled, + proj_man->value); + } + bpt_log(error, " Error: .bold.red[{}]"_styled, msg.value); + write_error_marker("invalid-dep-shorthand"); + return 1; + }, + [](const semver::invalid_version& exc, + e_parse_dep_shorthand_string given, + e_parse_dep_range_shorthand_string const* range_part, + e_parse_project_manifest_path const* proj_man) { + bpt_log(error, + "Invalid dependency shorthand string '.bold.yellow[{}]'"_styled, + given.value); + if (range_part) { + bpt_log(error, + " (While parsing name+range string '.bold.yellow[{}]')"_styled, + range_part->value); + } + if (proj_man) { + bpt_log(error, + " (While reading project manifest from [.bold.yellow[{}]]"_styled, + proj_man->value); + } + bpt_log(error, " Error: .bold.red[{}]"_styled, exc.what()); + write_error_marker("invalid-dep-shorthand"); + return 1; + }, + + [](e_nonesuch_library missing_lib) { + bpt_log(error, + "No such library .bold.red[{}] in package .bold.red[{}]"_styled, + missing_lib.value.name, + missing_lib.value.namespace_); + write_error_marker("no-such-library"); + return 1; + }, + [](bpt::e_exit ex, boost::leaf::verbose_diagnostic_info const& info) { + bpt_log(trace, "Additional error information: {}", info); + return ex.value; + }, + [](bpt::e_human_message message, + bpt::e_doc_ref docref, + boost::leaf::verbose_diagnostic_info const& diag, + bpt::e_error_marker const* marker) { + bpt_log(error, "Error: .bold.red[{}]"_styled, message.value); + bpt_log(error, "Refer: .bold.yellow[https://dds`.pizza/docs/{}]"_styled, docref.value); + bpt_log(debug, "Additional diagnostic objects:\n.blue[{}]"_styled, diag); + if (marker) { + bpt::write_error_marker(marker->value); + } + return 1; + }, + [](catch_ exc, boost::leaf::verbose_diagnostic_info const& diag) { + bpt_log(critical, + "An unhandled SQLite error arose. .bold.red[THIS IS A BPT BUG!] Info: {}"_styled, + diag); + bpt_log(critical, + "Exception message from neo::sqlite3::error: .bold.red[{}]"_styled, + exc.matched.what()); + return 42; + }, + [](const std::system_error& exc, boost::leaf::verbose_diagnostic_info const& diag) { + bpt_log( + critical, + "An unhandled std::system_error arose. .bold.red[THIS IS A BPT BUG!] Info: {}"_styled, + diag); + bpt_log(critical, + "Exception message from std::system_error: .bold.red[{}]"_styled, + exc.code().message()); + return 42; + }, + [](error_base const& exc, boost::leaf::verbose_diagnostic_info const& diag) { + bpt_log(error, "{}", exc.what()); + bpt_log(error, "{}", exc.explanation()); + bpt_log(error, "Refer: {}", exc.error_reference()); + bpt_log(debug, "Additional diagnostic details:\n.br.blue[{}]"_styled, diag); + return 1; + }, + [](boost::leaf::verbose_diagnostic_info const& diag) { + bpt_log(critical, + "An unhandled error arose. .bold.red[THIS IS A BPT BUG!] Info: {}"_styled, + diag); + return 42; + }); +} // namespace + +int bpt::handle_cli_errors(std::function fn) noexcept { + return boost::leaf::try_catch(fn, handlers); +} diff --git a/src/dds/cli/error_handler.hpp b/src/bpt/cli/error_handler.hpp similarity index 72% rename from src/dds/cli/error_handler.hpp rename to src/bpt/cli/error_handler.hpp index e0cd24f6..3e95cf9e 100644 --- a/src/dds/cli/error_handler.hpp +++ b/src/bpt/cli/error_handler.hpp @@ -2,8 +2,8 @@ #include -namespace dds { +namespace bpt { int handle_cli_errors(std::function) noexcept; -} // namespace dds +} // namespace bpt diff --git a/src/bpt/cli/options.cpp b/src/bpt/cli/options.cpp new file mode 100644 index 00000000..890b86f2 --- /dev/null +++ b/src/bpt/cli/options.cpp @@ -0,0 +1,528 @@ +#include "./options.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace bpt; +using namespace debate; +using namespace fansi::literals; + +namespace { + +struct setup { + bpt::cli::options& opts; + + explicit setup(bpt::cli::options& opts) + : opts(opts) {} + + // Util argument common to a lot of operations + argument if_exists_arg{ + .long_spellings = {"if-exists"}, + .help = "What to do if the resource already exists", + .valname = "{replace,ignore,fail}", + .action = put_into(opts.if_exists), + }; + + argument if_missing_arg{ + .long_spellings = {"if-missing"}, + .help = "What to do if the resource does not exist", + .valname = "{fail,ignore}", + .action = put_into(opts.if_missing), + }; + + argument toolchain_arg{ + .long_spellings = {"toolchain"}, + .short_spellings = {"t"}, + .help = "The toolchain to use when building", + .valname = "", + .action = put_into(opts.toolchain), + }; + + argument project_arg{ + .long_spellings = {"project"}, + .short_spellings = {"p"}, + .help = "The project to build. If not given, uses the current working directory", + .valname = "", + .action = put_into(opts.project_dir), + }; + + argument no_warn_arg{ + .long_spellings = {"no-warn", "no-warnings"}, + .help = "Disable build warnings", + .nargs = 0, + .action = store_true(opts.disable_warnings), + }; + + argument out_arg{ + .long_spellings = {"out", "output"}, + .short_spellings = {"o"}, + .help = "Path to the output", + .valname = "", + .action = put_into(opts.out_path), + }; + + argument jobs_arg{ + .long_spellings = {"jobs"}, + .short_spellings = {"j"}, + .help = "Set the maximum number of parallel jobs to execute", + .valname = "", + .action = put_into(opts.jobs), + }; + + argument repo_repo_dir_arg{ + .help = "The directory of the repository to manage", + .valname = "", + .required = true, + .action = put_into(opts.repo.repo_dir), + }; + + argument tweaks_dir_arg{ + .long_spellings = {"tweaks-dir"}, + .short_spellings = {"TD"}, + .help + = "Base directory of " + "\x1b]8;;https://vector-of-bool.github.io/2020/10/04/lib-configuration.html\x1b\\tweak " + "headers\x1b]8;;\x1b\\ that should be available to the build.", + .valname = "", + .action = put_into(opts.build.tweaks_dir), + }; + + argument use_repos_arg{ + .long_spellings = {"use-repo"}, + .short_spellings = {"r"}, + .help = "Use the given repository when resolving dependencies", + .valname = "", + .can_repeat = true, + .action = push_back_onto(opts.use_repos), + }; + + argument no_default_repo_arg{ + .long_spellings = {"no-default-repo"}, + .short_spellings = {"NDR"}, + .help = "Do not consult the default package repository [repo-3.bpt.pizza]", + .nargs = 0, + .action = store_false(opts.use_default_repo), + }; + + argument repo_sync_arg{ + .long_spellings = {"repo-sync"}, + .help + = "Mode for repository synchronization. Default is 'always'.\n" + "\n" + "always:\n Attempt to pull repository metadata. If that fails, fail immediately and " + "unconditionally.\n\n" + "cached-okay:\n Attempt to pull repository metadata. If a low-level network error " + "occurs and \n we have cached metadata for the failing repitory, ignore the error and " + "continue.\n\n" + "never:\n Do not attempt to pull repository metadata. This option requires that there " + "be a local cache of the repository metadata.", + .valname = "{always,cached-okay,never}", + .action = put_into(opts.repo_sync_mode), + }; + + void do_setup(argument_parser& parser) noexcept { + parser.add_argument({ + .long_spellings = {"log-level"}, + .short_spellings = {"l"}, + .help = "Set the bpt logging level. One of 'trace', 'debug', 'info', \n" + "'warn', 'error', 'critical', or 'silent'", + .valname = "", + .action = put_into(opts.log_level), + }); + parser.add_argument({ + .long_spellings = {"crs-cache-dir"}, + .help = "(Advanced) Override bpt's CRS caching directory.", + .valname = "", + .action = put_into(opts.crs_cache_dir), + }); + + setup_main_commands(parser.add_subparsers({ + .description = "The operation to perform", + .action = put_into(opts.subcommand), + })); + } + + void setup_main_commands(subparser_group& group) { + setup_build_cmd(group.add_parser({ + .name = "build", + .help = "Build a project", + })); + setup_compile_file_cmd(group.add_parser({ + .name = "compile-file", + .help = "Compile individual files in the project", + })); + setup_build_deps_cmd(group.add_parser({ + .name = "build-deps", + .help = "Build a set of dependencies and generate a libman index", + })); + setup_pkg_cmd(group.add_parser({ + .name = "pkg", + .help = "Manage packages and package remotes", + })); + setup_repo_cmd(group.add_parser({ + .name = "repo", + .help = "Manage a CRS package repository", + })); + setup_install_yourself_cmd(group.add_parser({ + .name = "install-yourself", + .help = "Have this bpt executable install itself onto your PATH", + })); + setup_new_cmd(group.add_parser({ + .name = "new", + .help = "Generate a new bpt project", + })); + } + + void add_repo_args(argument_parser& cmd) { + cmd.add_argument(use_repos_arg.dup()); + cmd.add_argument(no_default_repo_arg.dup()); + cmd.add_argument(repo_sync_arg.dup()); + } + + void setup_build_cmd(argument_parser& build_cmd) { + build_cmd.add_argument(toolchain_arg.dup()); + build_cmd.add_argument(project_arg.dup()); + add_repo_args(build_cmd); + build_cmd.add_argument({ + .long_spellings = {"no-tests"}, + .help = "Do not build and run project tests", + .nargs = 0, + .action = debate::store_false(opts.build.want_tests), + }); + build_cmd.add_argument({ + .long_spellings = {"no-apps"}, + .help = "Do not build project applications", + .nargs = 0, + .action = debate::store_false(opts.build.want_apps), + }); + build_cmd.add_argument(no_warn_arg.dup()); + build_cmd.add_argument(out_arg.dup()).help = "Directory where bpt will write build results"; + + build_cmd.add_argument(jobs_arg.dup()); + build_cmd.add_argument(tweaks_dir_arg.dup()); + } + + void setup_compile_file_cmd(argument_parser& compile_file_cmd) noexcept { + compile_file_cmd.add_argument(project_arg.dup()); + compile_file_cmd.add_argument(toolchain_arg.dup()); + compile_file_cmd.add_argument(no_warn_arg.dup()).help = "Disable compiler warnings"; + compile_file_cmd.add_argument(jobs_arg.dup()).help + = "Set the maximum number of files to compile in parallel"; + compile_file_cmd.add_argument(out_arg.dup()); + compile_file_cmd.add_argument(tweaks_dir_arg.dup()); + add_repo_args(compile_file_cmd); + compile_file_cmd.add_argument({ + .help = "One or more source files to compile", + .valname = "", + .can_repeat = true, + .action = debate::push_back_onto(opts.compile_file.files), + }); + } + + void setup_build_deps_cmd(argument_parser& build_deps_cmd) noexcept { + build_deps_cmd.add_argument(toolchain_arg.dup()).required; + build_deps_cmd.add_argument(jobs_arg.dup()); + build_deps_cmd.add_argument(out_arg.dup()); + build_deps_cmd.add_argument({ + .long_spellings = {"built-json"}, + .help = "Destination of the generated '_built.json' file.", + .valname = "", + .action = put_into(opts.build.built_json), + }); + add_repo_args(build_deps_cmd); + build_deps_cmd.add_argument({ + .long_spellings = {"deps-file"}, + .short_spellings = {"d"}, + .help = "Path to a YAML file listing dependencies", + .valname = "", + .can_repeat = true, + .action = debate::push_back_onto(opts.build_deps.deps_files), + }); + build_deps_cmd.add_argument({ + .long_spellings = {"cmake"}, + .help = "Generate a CMake file at the given path that will create import targets for " + "the dependencies", + .valname = "", + .action = debate::put_into(opts.build_deps.cmake_file), + }); + build_deps_cmd.add_argument(tweaks_dir_arg.dup()); + build_deps_cmd.add_argument({ + .help = "Dependency statement strings", + .valname = "", + .can_repeat = true, + .action = debate::push_back_onto(opts.build_deps.deps), + }); + } + + void setup_pkg_cmd(argument_parser& pkg_cmd) { + auto& pkg_group = pkg_cmd.add_subparsers({ + .valname = "", + .action = put_into(opts.pkg.subcommand), + }); + setup_pkg_create_cmd(pkg_group.add_parser({ + .name = "create", + .help = "Create a source distribution archive of a project", + })); + setup_pkg_search_cmd(pkg_group.add_parser({ + .name = "search", + .help = "Search for packages available to download", + })); + setup_pkg_prefetch_cmd(pkg_group.add_parser({ + .name = "prefetch", + .help = "Sync a remote repository into the local package listing cache", + })); + setup_pkg_solve_cmd(pkg_group.add_parser({ + .name = "solve", + .help = "Generate a dependency solution for the given requirements", + })); + } + + void setup_pkg_create_cmd(argument_parser& pkg_create_cmd) { + pkg_create_cmd.add_argument(project_arg.dup()).help + = "Path to the project for which to create a source distribution.\n" + "Default is the current working directory."; + pkg_create_cmd.add_argument(out_arg.dup()).help + = "Destination path for the source distribution archive"; + pkg_create_cmd.add_argument(if_exists_arg.dup()).help + = "What to do if the destination names an existing file"; + pkg_create_cmd.add_argument({ + .long_spellings = {"revision"}, + .help = "The revision number of the generated package (The CRS \"pkg-version\")", + .action = put_into(opts.pkg.create.revision), + }); + } + + void setup_pkg_search_cmd(argument_parser& pkg_search_cmd) noexcept { + add_repo_args(pkg_search_cmd); + pkg_search_cmd.add_argument({ + .help + = "A name or glob-style pattern. Only matching packages will be returned. \n" + "Searching is case-insensitive. Only the .italic[name] will be matched (not the \n" + "version).\n\nIf this parameter is omitted, the search will return .italic[all] \n" + "available packages."_styled, + .valname = "", + .action = put_into(opts.pkg.search.pattern), + }); + } + + void setup_pkg_prefetch_cmd(argument_parser& pkg_prefetch_cmd) noexcept { + add_repo_args(pkg_prefetch_cmd); + pkg_prefetch_cmd.add_argument({ + .help = "List of package IDs to prefetch", + .valname = "", + .can_repeat = true, + .action = push_back_onto(opts.pkg.prefetch.pkgs), + }); + } + + void setup_pkg_solve_cmd(argument_parser& pkg_solve_cmd) noexcept { + add_repo_args(pkg_solve_cmd); + pkg_solve_cmd.add_argument({ + .help = "List of package requirements to solve for", + .valname = "", + .required = true, + .can_repeat = true, + .action = push_back_onto(opts.pkg.solve.reqs), + }); + } + + void setup_repo_cmd(argument_parser& repo_cmd) noexcept { + auto& grp = repo_cmd.add_subparsers({ + .valname = "", + .action = put_into(opts.repo.subcommand), + }); + setup_repo_init_cmd(grp.add_parser({ + .name = "init", + .help = "Initialize a directory as a new CRS repository", + })); + setup_repo_import_cmd(grp.add_parser({ + .name = "import", + .help = "Import a directory or package into a CRS repository", + })); + setup_repo_remove_cmd(grp.add_parser({ + .name = "remove", + .help = "Remove packages from a CRS repository", + })); + auto& ls_cmd = grp.add_parser({ + .name = "ls", + .help = "List the packages in a local CRS repository", + }); + ls_cmd.add_argument(repo_repo_dir_arg.dup()); + auto& validate_cmd = grp.add_parser({ + .name = "validate", + .help = "Check that all repository packages are valid and resolvable", + }); + validate_cmd.add_argument(repo_repo_dir_arg.dup()); + } + + void setup_repo_import_cmd(argument_parser& repo_import_cmd) { + repo_import_cmd.add_argument(repo_repo_dir_arg.dup()); + repo_import_cmd.add_argument(if_exists_arg.dup()).help + = "Behavior when the package already exists in the repository"; + repo_import_cmd.add_argument({ + .help = "Paths of CRS directories to import", + .valname = "", + .can_repeat = true, + .action = push_back_onto(opts.repo.import.files), + }); + } + + void setup_repo_init_cmd(argument_parser& repn_init_cmd) { + repn_init_cmd.add_argument(repo_repo_dir_arg.dup()); + repn_init_cmd.add_argument(if_exists_arg.dup()).help + = "What to do if the directory exists and is already a repository"; + repn_init_cmd.add_argument({ + .long_spellings = {"name"}, + .short_spellings = {"n"}, + .help = "Specifiy the name of the new repository", + .valname = "", + .required = true, + .action = put_into(opts.repo.init.name), + }); + } + + void setup_repo_remove_cmd(argument_parser& repo_remove_cmd) { + repo_remove_cmd.add_argument(repo_repo_dir_arg.dup()); + repo_remove_cmd.add_argument(if_missing_arg.dup()).help + = "What to do if the request package does not exist in the repository"; + repo_remove_cmd.add_argument({ + .help = "One or more identifiers of packages to remove", + .valname = "", + .can_repeat = true, + .action = push_back_onto(opts.repo.remove.pkgs), + }); + } + + void setup_install_yourself_cmd(argument_parser& install_yourself_cmd) { + install_yourself_cmd.add_argument({ + .long_spellings = {"where"}, + .help = "The scope of the installation. For .bold[system], installs in a global \n" + "directory for all users of the system. For .bold[user], installs in a \n" + "user-specific directory for executable binaries."_styled, + .valname = "{user,system}", + .action = put_into(opts.install_yourself.where), + }); + install_yourself_cmd.add_argument({ + .long_spellings = {"dry-run"}, + .help + = "Do not actually perform any operations, but log what .italic[would] happen"_styled, + .nargs = 0, + .action = store_true(opts.dry_run), + }); + install_yourself_cmd.add_argument({ + .long_spellings = {"no-modify-path"}, + .help = "Do not attempt to modify the PATH environment variable", + .nargs = 0, + .action = store_false(opts.install_yourself.fixup_path_env), + }); + install_yourself_cmd.add_argument({ + .long_spellings = {"symlink"}, + .help = "Create a symlink at the installed location to the existing 'bpt' executable\n" + "instead of copying the executable file", + .nargs = 0, + .action = store_true(opts.install_yourself.symlink), + }); + } + + void setup_new_cmd(argument_parser& parser) { + parser.add_argument({ + .help = "Name for the new project", + .valname = "", + .action = put_into(opts.new_.name), + }); + parser.add_argument({ + .long_spellings = {"dir"}, + .help = "Directory in which the project will be generated", + .valname = "", + .action = put_into(opts.new_.directory), + }); + parser.add_argument({ + .long_spellings = {"split-src-include"}, + .help = "Whether to split the [src/] and [include/] directories", + .valname = "{true,false}", + .action = parse_bool_into(opts.new_.split_src_include), + }); + } +}; + +} // namespace + +void cli::options::setup_parser(debate::argument_parser& parser) noexcept { + setup{*this}.do_setup(parser); +} + +toolchain bpt::cli::options::load_toolchain() const { + if (!toolchain) { + return bpt::toolchain::get_default(); + } + // Convert the given string to a toolchain + auto& tc_str = *toolchain; + BPT_E_SCOPE(e_loading_toolchain{tc_str}); + if (tc_str.starts_with(":")) { + auto default_tc = tc_str.substr(1); + return bpt::toolchain::get_builtin(default_tc); + } else { + BPT_E_SCOPE(bpt::e_toolchain_filepath{tc_str}); + return toolchain::from_file(fs::path(tc_str)); + } +} + +fs::path bpt::cli::options::absolute_project_dir_path() const noexcept { + return bpt::resolve_path_weak(project_dir); +} + +bool cli::options::default_from_env(std::string key, bool def) noexcept { + auto env = bpt::getenv(key); + if (env.has_value()) { + return is_truthy_string(*env); + } + return def; +} + +std::string cli::options::default_from_env(std::string key, std::string def) noexcept { + return bpt::getenv(key).value_or(def); +} + +int cli::options::default_from_env(std::string key, int def) noexcept { + auto env = bpt::getenv(key); + if (!env.has_value()) { + return def; + } + int r = 0; + const auto dat = env->data(); + const auto dat_end = dat + env->size(); + auto res = std::from_chars(dat, dat_end, r); + if (res.ptr != dat_end) { + return def; + } + return r; +} + +cli::options::options() noexcept { + crs_cache_dir + = bpt::getenv("BPT_CRS_CACHE_DIR", [] { return crs::cache::default_path().string(); }); + + auto ll = getenv("BPT_LOG_LEVEL"); + if (ll.has_value()) { + auto llo = magic_enum::enum_cast(*ll); + if (llo.has_value()) { + log_level = *llo; + } + } + + toolchain = getenv("BPT_TOOLCHAIN"); + auto out = getenv("BPT_OUTPUT_PATH"); + if (out.has_value()) { + out_path = *out; + } +} diff --git a/src/dds/cli/options.hpp b/src/bpt/cli/options.hpp similarity index 55% rename from src/dds/cli/options.hpp rename to src/bpt/cli/options.hpp index 6a89a91b..38172ffc 100644 --- a/src/dds/cli/options.hpp +++ b/src/bpt/cli/options.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include @@ -8,16 +8,15 @@ #include #include -namespace dds { +namespace bpt { namespace fs = std::filesystem; -class pkg_db; class toolchain; namespace cli { /** - * @brief Top-level dds subcommands + * @brief Top-level bpt subcommands */ enum class subcommand { _none_, @@ -25,25 +24,24 @@ enum class subcommand { compile_file, build_deps, pkg, - repoman, + repo, install_yourself, + new_, }; /** - * @brief 'dds pkg' subcommands + * @brief 'bpt pkg' subcommands */ enum class pkg_subcommand { _none_, - ls, - get, create, - import, - repo, search, + prefetch, + solve, }; /** - * @brief 'dds pkg repo' subcommands + * @brief 'bpt pkg repo' subcommands */ enum class pkg_repo_subcommand { _none_, @@ -54,15 +52,15 @@ enum class pkg_repo_subcommand { }; /** - * @brief 'dds repoman' subcommands + * @brief 'bpt repo' subcommands * */ -enum class repoman_subcommand { +enum class repo_subcommand { _none_, init, import, - add, remove, + validate, ls, }; @@ -80,8 +78,14 @@ enum class if_missing { ignore, }; +enum class repo_sync_mode { + always, + cached_okay, + never, +}; + /** - * @brief Complete aggregate of all dds command-line options, and some utilities + * @brief Complete aggregate of all bpt command-line options, and some utilities */ struct options { using path = fs::path; @@ -89,16 +93,21 @@ struct options { using string = std::string; using opt_string = std::optional; - // The `--data-dir` argument - opt_path data_dir; - // The `--pkg-cache-dir' argument - opt_path pkg_cache_dir; - // The `--pkg-db-dir` argument - opt_path pkg_db_dir; + options() noexcept; + + // The `--crs-cache-dir` argument + path crs_cache_dir; // The `--log-level` argument log::level log_level = log::level::info; // Any `--dry-run` argument bool dry_run = false; + // A `--repo-sync-mode` argument + cli::repo_sync_mode repo_sync_mode = cli::repo_sync_mode::always; + + // All `--use-repo` arguments + std::vector use_repos; + // Toggle on/off the default repository + bool use_default_repo = !default_from_env("BPT_NO_DEFAULT_REPO", false); // The top-most selected subcommand enum subcommand subcommand; @@ -106,10 +115,13 @@ struct options { // Many subcommands use a '--project' argument, stored here, using the CWD as the default path project_dir = fs::current_path(); + // Obtain the absolute path specified by 'project_dir' (resolve using the CWD) + path absolute_project_dir_path() const noexcept; + // Compile and build commands with `--no-warnings`/`--no-warn` bool disable_warnings = false; // Compile and build commands' `--jobs` parameter - int jobs = 0; + int jobs = default_from_env("BPT_JOBS", 0); // Compile and build commands' `--toolchain` option: opt_string toolchain; opt_path out_path; @@ -120,30 +132,23 @@ struct options { cli::if_missing if_missing = cli::if_missing::fail; /** - * @brief Open the package pkg_db based on the user-specified options. - * @return pkg_db + * @brief Load a bpt toolchain as specified by the user, or a default. + * @return bpt::toolchain */ - pkg_db open_pkg_db() const; - /** - * @brief Load a dds toolchain as specified by the user, or a default. - * @return dds::toolchain - */ - dds::toolchain load_toolchain() const; + bpt::toolchain load_toolchain() const; /** - * @brief Parameters specific to 'dds build' + * @brief Parameters specific to 'bpt build' */ struct { - bool want_tests = true; - bool want_apps = true; - opt_path lm_index; - std::vector add_repos; - bool update_repos = false; - opt_path tweaks_dir; + bool want_tests = true; + bool want_apps = true; + opt_path built_json; + opt_path tweaks_dir; } build; /** - * @brief Parameters specific to 'dds compile-file' + * @brief Parameters specific to 'bpt compile-file' */ struct { /// The files that the user has requested to be compiled @@ -151,7 +156,7 @@ struct options { } compile_file; /** - * @brief Parameters specific to 'dds build-deps' + * @brief Parameters specific to 'bpt build-deps' */ struct { /// Files listed with '--deps-file' @@ -163,100 +168,70 @@ struct options { } build_deps; /** - * @brief Parameters and subcommands for 'dds pkg' + * @brief Parameters and subcommands for 'bpt pkg' * */ struct { - /// The 'dds pkg' subcommand + /// The 'bpt pkg' subcommand pkg_subcommand subcommand; - /** - * @brief Parameters for 'dds pkg import' - */ - struct { - /// File paths or URLs of packages to import - std::vector items; - /// Allow piping a package tarball in through stdin - bool from_stdin = false; - } import; - - /** - * @brief Parameters for 'dds pkg repo' - */ struct { - /// The 'pkg repo' subcommand - pkg_repo_subcommand subcommand; - - /** - * @brief Parameters of 'dds pkg repo add' - */ - struct { - /// The repository URL - string url; - /// Whether we should update repo data after adding the repository - bool update = true; - } add; - - /** - * @brief Parameters of 'dds pkg repo remove' - */ - struct { - /// Repositories to remove (by name) - std::vector names; - } remove; - } repo; + int revision = 1; + } create; /** - * @brief Paramters for 'dds pkg get' + * @brief Paramters for 'bpt pkg prefetch' */ struct { /// Package IDs to download std::vector pkgs; - } get; + } prefetch; /** - * @brief Parameters for 'dds pkg search' + * @brief Parameters for 'bpt pkg search' */ struct { /// The search pattern, if provided opt_string pattern; } search; + + /** + * @brief Paramters for 'bpt pkg solve' + */ + struct { + /// Requirements listed to solve + std::vector reqs; + } solve; } pkg; /** - * @brief Parameters for 'dds repoman' + * @brief Parameters for 'bpt repo' */ struct { - /// Shared parameter between repoman subcommands: The directory we are acting upon + /// Shared parameter between repo subcommands: The directory we are acting upon path repo_dir; /// The actual operation we are performing on the repository dir - repoman_subcommand subcommand; + repo_subcommand subcommand; - /// Options for 'dds repoman init' + /// Options for 'bpt repo init' struct { /// The name of the new repository. If not provided, a random one will be generated - opt_string name; + string name; } init; - /// Options for 'dds repoman import' + /// Options for 'bpt repo import' struct { /// sdist tarball file paths to import into the repository std::vector files; } import; - /// Options for 'dds repoman add' - struct { - std::string url_str; - std::string description; - } add; - - /// Options for 'dds repoman remove' + /// Options for 'bpt repo remove' struct { /// Package IDs of packages to remove std::vector pkgs; } remove; - } repoman; + } repo; struct { enum where_e { @@ -268,12 +243,25 @@ struct options { bool symlink = false; } install_yourself; + struct { + /// The directory in which to create the new project + opt_string directory; + /// The name of the new project + opt_string name; + /// Whether to split sources/headers + std::optional split_src_include; + } new_; + /** * @brief Attach arguments and subcommands to the given argument parser, binding those arguments * to the values in this object. */ void setup_parser(debate::argument_parser& parser) noexcept; + + static bool default_from_env(string env_var, bool default_value) noexcept; + static string default_from_env(string env_var, string default_value) noexcept; + static int default_from_env(string env_var, int default_value) noexcept; }; } // namespace cli -} // namespace dds +} // namespace bpt diff --git a/src/dds/compdb.cpp b/src/bpt/compdb.cpp similarity index 68% rename from src/dds/compdb.cpp rename to src/bpt/compdb.cpp index 0eb8342e..5cdda589 100644 --- a/src/dds/compdb.cpp +++ b/src/bpt/compdb.cpp @@ -1,14 +1,15 @@ #include "./compdb.hpp" -#include -#include -#include +#include +#include +#include +#include #include -using namespace dds; +using namespace bpt; -void dds::generate_compdb(const build_plan& plan, build_env_ref env) { +void bpt::generate_compdb(const build_plan& plan, build_env_ref env) { auto compdb = nlohmann::json::array(); for (const compile_file_plan& cf : iter_compilations(plan)) { @@ -23,6 +24,6 @@ void dds::generate_compdb(const build_plan& plan, build_env_ref env) { fs::create_directories(env.output_root); auto compdb_file = env.output_root / "compile_commands.json"; - auto ostream = open(compdb_file, std::ios::binary | std::ios::out); + auto ostream = bpt::open_file(compdb_file, std::ios::binary | std::ios::out); ostream << compdb.dump(2); } \ No newline at end of file diff --git a/src/dds/compdb.hpp b/src/bpt/compdb.hpp similarity index 51% rename from src/dds/compdb.hpp rename to src/bpt/compdb.hpp index 199ed429..c640b3c4 100644 --- a/src/dds/compdb.hpp +++ b/src/bpt/compdb.hpp @@ -1,9 +1,9 @@ #pragma once -#include +#include -namespace dds { +namespace bpt { void generate_compdb(const build_plan&, build_env_ref); -} // namespace dds \ No newline at end of file +} // namespace bpt \ No newline at end of file diff --git a/src/bpt/config.cpp b/src/bpt/config.cpp new file mode 100644 index 00000000..e36dcb12 --- /dev/null +++ b/src/bpt/config.cpp @@ -0,0 +1,5 @@ +#include "./config.hpp" + +#include + +bool bpt::config::defaults::enable_sqlite3_trace() { return bpt::getenv_bool("BPT_SQLITE3_TRACE"); } diff --git a/src/bpt/config.hpp b/src/bpt/config.hpp new file mode 100644 index 00000000..5dee52f5 --- /dev/null +++ b/src/bpt/config.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +namespace bpt::config { + +namespace defaults { + +/** + * @brief Control whether --log-level=trace writes log events for SQLite statement step() + * invocations. This default returns true if the BPT_SQLITE3_TRACE environment variable is + * set to a truthy value. + */ +bool enable_sqlite3_trace(); + +} // namespace defaults + +using namespace defaults; + +} // namespace bpt::config diff --git a/src/bpt/crs/cache.cpp b/src/bpt/crs/cache.cpp new file mode 100644 index 00000000..51155a3b --- /dev/null +++ b/src/bpt/crs/cache.cpp @@ -0,0 +1,66 @@ +#include "./cache.hpp" + +#include "./cache_db.hpp" +#include "./remote.hpp" +#include +#include +#include +#include + +#include +#include + +#include + +using namespace bpt; +using namespace bpt::crs; +using namespace neo::sqlite3::literals; +using namespace fansi::literals; + +struct cache::impl { + fs::path root_dir; + unique_database db = unique_database::open((root_dir / "bpt-metadata.db").string()).value(); + cache_db metadata_db = cache_db::open(db); + file_collector fcoll = file_collector::create(db); + + explicit impl(fs::path p) + : root_dir(p) { + db.exec_script("PRAGMA journal_mode = WAL"_sql); + } +}; + +cache cache::open(path_ref dirpath) { + fs::create_directories(dirpath); + return cache{dirpath}; +} + +cache::cache(path_ref dirpath) + : _impl(std::make_shared(dirpath)) {} + +cache_db& cache::db() noexcept { return _impl->metadata_db; } + +fs::path cache::prefetch(const pkg_id& pid_) { + auto pid = pid_; + auto entries = db().for_package(pid.name, pid.version); + auto it = entries.begin(); + if (it == entries.end()) { + BOOST_LEAF_THROW_EXCEPTION(e_no_such_pkg{pid}); + } + auto remote = db().get_remote_by_id(it->remote_id); + if (pid.revision == 0) { + pid.revision = it->pkg.id.revision; + } + auto pkg_dir = _impl->root_dir / "pkgs" / pid.to_string(); + if (fs::exists(pkg_dir)) { + return pkg_dir; + } + neo_assert(invariant, + remote.has_value(), + "Unable to get the remote of a just-obtained package entry", + pid.to_string()); + bpt_log(info, "Fetching package .br.cyan[{}]"_styled, pid.to_string()); + crs::pull_pkg_from_remote(pkg_dir, remote->url, pid); + return pkg_dir; +} + +fs::path cache::default_path() noexcept { return bpt::bpt_cache_dir() / "crs"; } diff --git a/src/bpt/crs/cache.hpp b/src/bpt/crs/cache.hpp new file mode 100644 index 00000000..03e69f02 --- /dev/null +++ b/src/bpt/crs/cache.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include +#include + +namespace bpt::crs { + +class cache_db; +struct pkg_id; + +/** + * @brief Implements a cache of CRS source distributions along with a cache of CRS metadata from + * some number of repositories. + * + * The packages available for use are controlled with a @ref cache_db that is stored + * in the cache directory. + */ +class cache { + struct impl; + std::shared_ptr _impl; + + explicit cache(std::filesystem::path const& p); + +public: + /** + * @brief Open a cache in the given directory + * + * @param dirpath A directory in which to create/open a CRS cache. If it does + * not exist, a new cache will be initialized. + */ + static cache open(std::filesystem::path const& dirpath); + + /** + * @brief Get the default directory for the CRS cache for the current user. + */ + static std::filesystem::path default_path() noexcept; + + /** + * @brief The CRS metadata database for this CRS cache. + * + * @return cache_db& + */ + cache_db& db() noexcept; + + /** + * @brief Ensure that the given package has a locally cached copy of its source distribution. + * + * If the package has already been pre-fetched, it will not be pulled again. + * + * @returns The directory of the source r for the requested package. + * + * @throws std::exception if the given package does not have CRS metadata in any of the + * currently enabled remotes for the metadata database, even if there is an existing local copy + * of the package. + */ + std::filesystem::path prefetch(const pkg_id&); +}; + +} // namespace bpt::crs diff --git a/src/bpt/crs/cache.test.cpp b/src/bpt/crs/cache.test.cpp new file mode 100644 index 00000000..c03dae29 --- /dev/null +++ b/src/bpt/crs/cache.test.cpp @@ -0,0 +1,13 @@ +#include "./cache.hpp" + +#include +#include + +#include + +TEST_CASE("Create a directory") { + auto tdir = bpt::temporary_dir::create(); + bpt::fs::create_directories(tdir.path()); + + REQUIRES_LEAF_NOFAIL(bpt::crs::cache::open(tdir.path())); +} diff --git a/src/bpt/crs/cache_db.cpp b/src/bpt/crs/cache_db.cpp new file mode 100644 index 00000000..45886c9a --- /dev/null +++ b/src/bpt/crs/cache_db.cpp @@ -0,0 +1,525 @@ +#include "./cache_db.hpp" + +#include "./error.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace bpt; +using namespace bpt::crs; +using namespace neo::sqlite3::literals; +using namespace fansi::literals; +using std::optional; +using std::string; +using std::string_view; +namespace chrono = std::chrono; +using chrono::steady_clock; +using steady_time_point = steady_clock::time_point; + +neo::sqlite3::statement& cache_db::_prepare(neo::sqlite3::sql_string_literal sql) const { + return _db.get().prepare(sql); +} + +cache_db cache_db::open(unique_database& db) { + bpt::apply_db_migrations(db, "bpt_crs_meta", [](auto& db) { + db.exec_script(R"( + CREATE TABLE bpt_crs_remotes ( + remote_id INTEGER PRIMARY KEY, + url TEXT NOT NULL, + unique_name TEXT NOT NULL UNIQUE, + revno INTEGER NOT NULL, + -- HTTP Etag header + etag TEXT, + -- HTTP Last-Modified header + last_modified TEXT, + -- System time of the most recent DB update + resource_time INTEGER, + -- Content of the prior Cache-Control header for HTTP remotes + cache_control TEXT + ); + + CREATE TABLE bpt_crs_packages ( + pkg_id INTEGER PRIMARY KEY, + json TEXT NOT NULL, + remote_id INTEGER NOT NULL + REFERENCES bpt_crs_remotes + ON DELETE CASCADE, + remote_revno INTEGER NOT NULL, + name TEXT NOT NULL + GENERATED ALWAYS + AS (json_extract(json, '$.name')) + STORED, + version TEXT NOT NULL + GENERATED ALWAYS + AS (json_extract(json, '$.version')) + STORED, + pkg_version INTEGER NOT NULL + GENERATED ALWAYS + AS (json_extract(json, '$.pkg-version')) + STORED, + UNIQUE (name, version, remote_id) + ); + )"_sql); + }).value(); + db.exec_script(R"( + CREATE TEMPORARY TABLE IF NOT EXISTS bpt_crs_enabled_remotes ( + enablement_id INTEGER PRIMARY KEY, + remote_id INTEGER NOT NULL -- references main.bpt_crs_remotes + UNIQUE ON CONFLICT IGNORE + ); + CREATE TEMPORARY VIEW IF NOT EXISTS enabled_packages AS + SELECT * FROM bpt_crs_packages + JOIN bpt_crs_enabled_remotes USING (remote_id) + ORDER BY enablement_id ASC + )"_sql); + return cache_db{db}; +} + +static cache_db::package_entry pkg_entry_from_row(auto&& row) noexcept { + auto&& [rowid, remote_id, json_str] = row; + auto meta = package_info::from_json_str(json_str); + return cache_db::package_entry{.package_id = rowid, + .remote_id = remote_id, + .pkg = std::move(meta)}; +} + +void cache_db::forget_all() { db_exec(_prepare("DELETE FROM bpt_crs_remotes"_sql)).value(); } + +namespace { + +neo::any_input_range +cache_entries_for_query(neo::sqlite3::statement&& st) { + return bpt_leaf_try->result> { + auto pin = neo::copy_shared(std::move(st)); + return neo::sqlite3::iter_tuples(*pin) + | std::views::transform([pin](auto row) { return pkg_entry_from_row(row); }) // + ; + } + bpt_leaf_catch(catch_ err)->noreturn_t { + BOOST_LEAF_THROW_EXCEPTION(err, err.matched.code()); + } + bpt_leaf_catch_all->noreturn_t { + neo_assert(invariant, + false, + "Unexpected exception while loading from CRS cache database", + diagnostic_info); + }; +} + +} // namespace + +neo::any_input_range +cache_db::for_package(bpt::name const& name, semver::version const& version) const { + neo_assertion_breadcrumbs("Loading package cache entries for name+version", + name.str, + version.to_string()); + auto st = *_db.get().sqlite3_db().prepare(R"( + SELECT pkg_id, remote_id, json + FROM enabled_packages + WHERE name = ? AND version = ? + ORDER BY pkg_version DESC + )"); + st.bindings() = std::forward_as_tuple(name.str, version.to_string()); + return cache_entries_for_query(std::move(st)); +} + +neo::any_input_range cache_db::for_package(bpt::name const& name) const { + neo_assertion_breadcrumbs("Loading package cache entries for name", name.str); + auto st = *_db.get().sqlite3_db().prepare(R"( + SELECT pkg_id, remote_id, json + FROM enabled_packages + WHERE name = ? + )"); + st.bindings() = std::forward_as_tuple(name.str); + db_bind(st, string_view(name.str)).value(); + return cache_entries_for_query(std::move(st)); +} + +neo::any_input_range cache_db::all_enabled() const { + neo_assertion_breadcrumbs("Loading all enabled package entries"); + auto st + = *_db.get().sqlite3_db().prepare("SELECT pkg_id, remote_id, json FROM enabled_packages"); + return cache_entries_for_query(std::move(st)); +} + +optional cache_db::get_remote(neo::url_view const& url_) const { + return bpt_leaf_try->optional { + auto url = url_.normalized(); + auto row = db_single( // + _prepare(R"( + SELECT remote_id, url, unique_name + FROM bpt_crs_remotes + WHERE url = ? + )"_sql), + string_view(url.to_string())) + .value(); + auto [rowid, url_str, name] = row; + return cache_db::remote_entry{rowid, bpt::parse_url(url_str), name}; + } + bpt_leaf_catch(matchv) { return std::nullopt; }; +} + +optional cache_db::get_remote_by_id(std::int64_t rowid) const { + auto row = neo::sqlite3::one_row( // + _prepare(R"( + SELECT url, unique_name + FROM bpt_crs_remotes WHERE remote_id = ? + )"_sql), + rowid); + if (row.has_value()) { + auto [url_str, name] = *row; + return remote_entry{rowid, bpt::parse_url(url_str), name}; + } + return std::nullopt; +} + +void cache_db::enable_remote(neo::url_view const& url_) { + auto url = url_.normalized(); + auto res = neo::sqlite3::one_row( // + _prepare(R"( + INSERT INTO bpt_crs_enabled_remotes (remote_id) + SELECT remote_id + FROM bpt_crs_remotes + WHERE url = ? + RETURNING remote_id + )"_sql), + string_view(url.to_string())); + if (res == neo::sqlite3::errc::done) { + BOOST_LEAF_THROW_EXCEPTION(e_no_such_remote_url{url.to_string()}); + } +} + +namespace { + +struct remote_repo_db_info { + fs::path local_path; + std::optional etag; + std::optional last_modified; + std::optional resource_time; + std::optional cache_control; + bool up_to_date; +}; + +/** + * @brief Determine whether we should revalidate a cached resource based on the cache-control and + * age of the resource. + * + * @param cache_control The 'Cache-Control' header + * @param resource_time The create-time of the resource. This may be affected by the 'Age' header. + */ +bool should_revalidate(std::string_view cache_control, steady_time_point resource_time) { + auto parts = bpt::split_view(cache_control, ", "); + if (std::ranges::find(parts, "no-cache", trim_view) != parts.end()) { + // Always revalidate + return true; + } + if (auto max_age = std::ranges::find_if(parts, BPT_TL(_1.starts_with("max-age="))); + max_age != parts.end()) { + auto age_str = bpt::trim_view(max_age->substr(std::strlen("max-age="))); + int max_age_int = 0; + auto r = std::from_chars(age_str.data(), age_str.data() + age_str.size(), max_age_int); + if (r.ec != std::errc{}) { + bpt_log(warn, + "Malformed max-age integer '{}' in stored cache-control header '{}'", + age_str, + cache_control); + return true; + } + auto stale_time = resource_time + chrono::seconds(max_age_int); + if (stale_time < steady_clock::now()) { + // The associated resource has become stale, so it must revalidate + return true; + } else { + // The cached item is still fresh + bpt_log(debug, "Cached repo data is still within its max-age."); + bpt_log(debug, + "Repository data will expire in {} seconds", + chrono::duration_cast(stale_time - steady_clock::now()) + .count()); + return false; + } + } + // No other headers supported yet. Just revalidate. + return true; +} + +neo::co_resource get_remote_db(bpt::unique_database& db, neo::url url) { + if (url.scheme == "file") { + auto path = fs::path(url.path); + bpt_log(info, "Importing local repository .cyan[{}] ..."_styled, url.path); + auto info = remote_repo_db_info{ + .local_path = path / "repo.db", + .etag = std::nullopt, + .last_modified = std::nullopt, + .resource_time = std::nullopt, + .cache_control = std::nullopt, + .up_to_date = false, + }; + co_yield info; + } else { + bpt::http_request_params params; + // Open the prior download info from the cachedb + auto& prio_info_st = db.prepare(R"( + SELECT etag, last_modified, resource_time, cache_control + FROM bpt_crs_remotes + WHERE url=?)"_sql); + auto url_str = url.to_string(); + neo_assertion_breadcrumbs("Pulling remote repository metadata", url_str); + bpt_log(debug, "Syncing repository [{}] via HTTP", url_str); + neo::sqlite3::reset_and_bind(prio_info_st, std::string_view(url_str)).throw_if_error(); + auto prior_info = neo::sqlite3::one_row, + std::optional, + std::int64_t, + std::optional>(prio_info_st); + if (prior_info.has_value()) { + bpt_log(debug, "Seen this remote repository before. Checking for updates."); + const auto& [etag, last_mod, prev_rc_time_, cache_control] = *prior_info; + if (etag.has_value()) { + params.prior_etag = *etag; + } + if (last_mod.has_value()) { + params.last_modified = *last_mod; + } + // Check if the cached item is stale according to the server. + const auto prev_rc_time = steady_time_point(steady_clock::duration(prev_rc_time_)); + if (cache_control and not should_revalidate(*cache_control, prev_rc_time)) { + bpt_log(info, "Repository data from .cyan[{}] is fresh"_styled, url_str); + auto info = remote_repo_db_info{ + .local_path = "", + .etag = etag, + .last_modified = last_mod, + .resource_time = prev_rc_time, + .cache_control = cache_control, + .up_to_date = true, + }; + co_yield info; + co_return; + } + } + + if (!prior_info.has_value() && prior_info.errc() != neo::sqlite3::errc::done) { + prior_info.throw_error(); + } + + auto& pool = bpt::http_pool::thread_local_pool(); + + auto repo_db_gz_url = url / "repo.db.gz"; + + bool do_discard = true; + request_result rinfo = pool.request(repo_db_gz_url, params); + neo_defer { + if (do_discard) { + rinfo.client.abort_client(); + } + }; + + if (auto message = rinfo.resp.header_value("x-bpt-user-message")) { + bpt_log(info, "Message from repository [{}]: {}", url_str, *message); + } + + // Compute the create-time of the source. By default, just the request time. + auto resource_time = steady_clock::now(); + if (auto age_str = rinfo.resp.header_value("Age")) { + int age_int = 0; + auto r = std::from_chars(age_str->data(), age_str->data() + age_str->size(), age_int); + if (r.ec == std::errc{}) { + resource_time -= chrono::seconds{age_int}; + } + } + + // Init the info about the resource that we will return to the caller + remote_repo_db_info yield_info{ + .local_path = "", + .etag = rinfo.resp.etag(), + .last_modified = rinfo.resp.last_modified(), + .resource_time = resource_time, + .cache_control = rinfo.resp.header_value("Cache-Control"), + .up_to_date = false, + }; + + if (rinfo.resp.not_modified()) { + bpt_log(info, "Repository data from .cyan[{}] is up-to-date"_styled, url_str); + do_discard = false; + rinfo.discard_body(); + yield_info.up_to_date = true; + co_yield yield_info; + co_return; + } + + bpt_log(info, "Syncing repository .cyan[{}] ..."_styled, url_str); + + // pool.request() will resolve redirects and errors + neo_assert(invariant, + !rinfo.resp.is_redirect(), + "Did not expect an HTTP redirect at this IO layer"); + neo_assert(invariant, + !rinfo.resp.is_error(), + "Did not expect an HTTP error at this IO layer"); + + auto tmpdir = bpt::temporary_dir::create(); + auto dest_file = tmpdir.path() / "repo.db.gz"; + std::filesystem::create_directories(tmpdir.path()); + rinfo.save_file(dest_file); + do_discard = false; + + auto repo_db = tmpdir.path() / "repo.db"; + bpt::decompress_file_gz(dest_file, repo_db).value(); + yield_info.local_path = repo_db; + co_yield yield_info; + } +} + +} // namespace + +void cache_db::sync_remote(const neo::url_view& url_) const { + bpt::unique_database& db = _db; + auto url = url_.normalized(); + BPT_E_SCOPE(e_sync_remote{url}); + auto remote_db = get_remote_db(_db, url); + + auto rc_time = remote_db->resource_time; + + if (remote_db->up_to_date) { + neo::sqlite3::exec( // + db.prepare("UPDATE bpt_crs_remotes " + "SET resource_time = ?1, cache_control = ?2"_sql), + rc_time ? std::make_optional(rc_time->time_since_epoch().count()) : std::nullopt, + remote_db->cache_control) + .throw_if_error(); + return; + } + + neo::sqlite3::exec(db.prepare("ATTACH DATABASE ? AS remote"_sql), + remote_db->local_path.string()) + .throw_if_error(); + neo_defer { db.exec_script(R"(DETACH DATABASE remote)"_sql); }; + + // Import those packages + neo::sqlite3::transaction_guard tr{db.sqlite3_db()}; + + auto& update_remote_st = db.prepare(R"( + INSERT INTO bpt_crs_remotes + (url, unique_name, revno, etag, last_modified, resource_time, cache_control) + VALUES ( + ?1, -- url + (SELECT name FROM remote.crs_repo_self), -- unique_name + 1, -- revno + ?2, -- etag + ?3, -- last_modified + ?4, -- resource_time + ?5 -- Cache-Control header + ) + ON CONFLICT (unique_name) DO UPDATE + SET url = ?1, + etag = ?2, + last_modified = ?3, + resource_time = ?4, + cache_control = ?5, + revno = revno + 1 + RETURNING remote_id, revno + )"_sql); + auto [remote_id, remote_revno] = *neo::sqlite3::one_row( // + update_remote_st, + url.to_string(), + remote_db->etag, + remote_db->last_modified, + rc_time ? std::make_optional(rc_time->time_since_epoch().count()) : std::nullopt, + remote_db->cache_control); + + auto remote_packages = // + *neo::sqlite3::exec_tuples(db.prepare(R"( + SELECT meta_json FROM remote.crs_repo_packages + )"_sql)) + | std::views::transform([](auto tup) -> std::optional { + auto [json_str] = tup; + return bpt_leaf_try->std::optional { + auto meta = package_info::from_json_str(json_str); + if (meta.id.revision < 1) { + bpt_log(warn, + "Remote package {} has an invalid 'pkg-version' of {}.", + meta.id.to_string(), + meta.id.revision); + bpt_log(warn, " The corresponding package will not be available."); + bpt_log(debug, " The bad JSON content is: {}", json_str); + return std::nullopt; + } + return meta; + } + bpt_leaf_catch(e_invalid_meta_data err) { + bpt_log(warn, "Remote package has an invalid JSON entry: {}", err.value); + bpt_log(warn, " The corresponding package will not be available."); + bpt_log(debug, " The bad JSON content is: {}", json_str); + return std::nullopt; + }; + }); + + auto n_before = *neo::sqlite3::one_cell( + db.prepare("SELECT count(*) FROM bpt_crs_packages"_sql)); + auto& update_pkg_st = db.prepare(R"( + INSERT INTO bpt_crs_packages (json, remote_id, remote_revno) + VALUES (?1, ?2, ?3) + ON CONFLICT(name, version, remote_id) DO UPDATE + SET json=excluded.json, remote_revno=?3 + WHERE json_extract(excluded.json, '$.pkg-version') >= pkg_version + )"_sql); + for (auto meta : remote_packages) { + if (!meta.has_value()) { + continue; + } + neo::sqlite3::exec(update_pkg_st, meta->to_json(), remote_id, remote_revno) + .throw_if_error(); + } + auto n_after = *neo::sqlite3::one_cell( + db.prepare("SELECT count(*) FROM bpt_crs_packages"_sql)); + const std::int64_t n_added = n_after - n_before; + + auto& delete_old_st = db.prepare(R"( + DELETE FROM bpt_crs_packages + WHERE remote_id = ? AND remote_revno < ? + )"_sql); + neo::sqlite3::reset_and_bind(delete_old_st, remote_id, remote_revno).throw_if_error(); + neo::sqlite3::exec(delete_old_st).throw_if_error(); + const auto n_deleted = db.sqlite3_db().changes(); + + bpt_log(debug, "Running integrity check"); + db.exec_script("PRAGMA main.integrity_check"_sql); + + bpt_log(info, + "Syncing repository .cyan[{}] Done: {} added, {} deleted"_styled, + url.to_string(), + n_added, + n_deleted); +} + +neo::sqlite3::connection_ref cache_db::sqlite3_db() const noexcept { + return _db.get().sqlite3_db(); +} \ No newline at end of file diff --git a/src/bpt/crs/cache_db.hpp b/src/bpt/crs/cache_db.hpp new file mode 100644 index 00000000..953cb2a0 --- /dev/null +++ b/src/bpt/crs/cache_db.hpp @@ -0,0 +1,120 @@ +#pragma once + +#include "./info/package.hpp" +#include "./info/pkg_id.hpp" + +#include + +#include +#include +#include +#include + +namespace bpt::crs { + +struct e_no_such_remote_url { + std::string value; +}; + +/** + * @brief An interface to a cache of CRS package metadata. + * + * The CRS cache records the totality of all known packages that are available to be imported and + * included in a potential build. Every package metadata entry corresponds to a remote package + * repository. + * + * When resolving a dependency tree, a single cache DB should be consulted to understand what + * packages are available to generate a dependency solution, regardless of whether they are (yet) + * locally available packages. + */ +class cache_db { + neo::ref_member _db; + + explicit cache_db(bpt::unique_database& db) noexcept + : _db(db) {} + + // Convenience method to return a prepared statement + neo::sqlite3::statement& _prepare(neo::sqlite3::sql_string_literal) const; + +public: + /** + * @brief A cache entry for a CRS package. + * + * Has a set of package metadata, and optionally a local path (where the package contents can be + * found), or a URL (from whence the package contents can be obtained). + */ + struct package_entry { + /// The database row ID of this package + std::int64_t package_id; + /// The row ID of the remote that contains this package + std::int64_t remote_id; + /// The metadata for this cache entry + package_info pkg; + }; + + /** + * @brief A database entry for a CRS remote repository + */ + struct remote_entry { + /// The database row ID of this package + std::int64_t remote_id; + /// The URL of this remote + neo::url url; + /// A globally unique name of this repository + std::string unique_name; + }; + + /** + * @brief Open a cache database for the given SQLite database. + */ + [[nodiscard]] static cache_db open(unique_database& db); + + [[nodiscard]] std::optional get_remote(neo::url_view const& url) const; + [[nodiscard]] std::optional get_remote_by_id(std::int64_t) const; + + void enable_remote(neo::url_view const&); + void disable_remote(neo::url_view const&); + + /** + * @brief Forget every cached entry in the database + */ + void forget_all(); + + /** + * @brief Obtain a list of package entries for the given name and version + * + * @param name The name of a package + * @param version The version of the package + * + * @note The package range returned is ordered in descending order by the pkg-version + */ + [[nodiscard]] neo::any_input_range + for_package(bpt::name const& name, semver::version const& version) const; + + /** + * @brief Obtain a list of package entries for the given name. + * + * All available versions will be returned. + * + * @param name The name of a package. + */ + [[nodiscard]] neo::any_input_range for_package(bpt::name const& name) const; + + /** + * @brief Iterate over all package entries that are currently available in any enabled remote. + * + * @return neo::any_input_range + */ + [[nodiscard]] neo::any_input_range all_enabled() const; + + /** + * @brief Ensure that we have up-to-date package metadata from the given remote repo + */ + void sync_remote(const neo::url_view& url) const; + + std::optional lowest_version_matching(const dependency& dep) const; + + neo::sqlite3::connection_ref sqlite3_db() const noexcept; +}; + +} // namespace bpt::crs diff --git a/src/bpt/crs/cache_db.test.cpp b/src/bpt/crs/cache_db.test.cpp new file mode 100644 index 00000000..0d6c2486 --- /dev/null +++ b/src/bpt/crs/cache_db.test.cpp @@ -0,0 +1,57 @@ +#include "./cache_db.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace neo::sqlite3::literals; + +TEST_CASE("Open a cache") { + auto db = REQUIRES_LEAF_NOFAIL(bpt::unique_database::open(":memory:").value()); + auto ldr = REQUIRES_LEAF_NOFAIL(bpt::crs::cache_db::open(db)); + (void)ldr; +} + +struct empty_loader { + bpt::unique_database db = bpt::unique_database::open(":memory:").value(); + bpt::crs::cache_db cache = bpt::crs::cache_db::open(db); +}; + +TEST_CASE_METHOD(empty_loader, "Get a non-existent remote") { + auto url = neo::url::parse("http://example.com"); + auto entry = REQUIRES_LEAF_NOFAIL(cache.get_remote(url)); + CHECK_FALSE(entry.has_value()); +} + +TEST_CASE_METHOD(empty_loader, "Sync an invalid repo") { + auto url = neo::url::parse("http://example.com"); + bpt_leaf_try { + cache.sync_remote(url); + FAIL_CHECK("Expected an error to occur"); + } + bpt_leaf_catch(const bpt::http_error& exc, + bpt::http_response_info resp, + bpt::matchv) { + CHECK(exc.status_code() == 404); + CHECK(resp.status == 404); + } + bpt_leaf_catch_all { FAIL_CHECK("Unhandled error: " << diagnostic_info); }; +} + +TEST_CASE_METHOD(empty_loader, "Enable an invalid repo") { + auto url = neo::url::parse("http://example.com"); + bpt_leaf_try { + cache.enable_remote(url); + FAIL_CHECK("Expected an error to occur"); + } + bpt_leaf_catch(const bpt::crs::e_no_such_remote_url e) { + CHECK(e.value == "http://example.com/"); + } + bpt_leaf_catch_all { FAIL_CHECK("Unhandled error: " << diagnostic_info); }; +} diff --git a/src/bpt/crs/error.hpp b/src/bpt/crs/error.hpp new file mode 100644 index 00000000..8042bd78 --- /dev/null +++ b/src/bpt/crs/error.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include "./info/package.hpp" + +#include + +#include + +namespace bpt::crs { + +struct e_repo_open_path { + std::filesystem::path value; +}; + +struct e_repo_importing_dir { + std::filesystem::path value; +}; + +struct e_repo_importing_package { + package_info value; +}; + +struct e_repo_already_init {}; + +struct e_repo_import_pkg_already_present {}; + +struct e_repo_import_invalid_pkg_version { + std::string value; +}; + +struct e_sync_remote { + neo::url value; +}; + +} // namespace bpt::crs \ No newline at end of file diff --git a/src/bpt/crs/info/dependency.cpp b/src/bpt/crs/info/dependency.cpp new file mode 100644 index 00000000..d07d54ab --- /dev/null +++ b/src/bpt/crs/info/dependency.cpp @@ -0,0 +1,144 @@ +#include "./dependency.hpp" + +#include + +#include +#include + +#include + +using namespace bpt; +using namespace bpt::crs; +using namespace bpt::walk_utils; + +namespace { + +std::string iv_string(const pubgrub::interval_set::interval_type& iv) { + if (iv.high == semver::version::max_version()) { + return ">=" + iv.low.to_string(); + } + if (iv.low == semver::version()) { + return "<" + iv.high.to_string(); + } + return iv.low.to_string() + " < " + iv.high.to_string(); +} + +semver::version next_major(semver::version const& v) { + auto v2 = v; + v2.minor = 0; + v2.patch = 0; + ++v2.major; + v2.prerelease = {}; + return v2; +} + +semver::version next_minor(semver::version const& v) { + auto v2 = v; + v2.patch = 0; + ++v2.minor; + v2.prerelease = {}; + return v2; +} + +} // namespace + +std::string bpt::crs::dependency::decl_to_string() const noexcept { + std::stringstream strm; + strm << name.str; + if (acceptable_versions.num_intervals() == 1) { + auto iv = *acceptable_versions.iter_intervals().begin(); + if (iv.high == iv.low.next_after()) { + strm << "@" << iv.low.to_string(); + } else if (iv.high == next_major(iv.low)) { + strm << "^" << iv.low.to_string(); + } else if (iv.high == next_minor(iv.low)) { + strm << "~" << iv.low.to_string(); + } else if (iv.low == semver::version() && iv.high == semver::version::max_version()) { + strm << "+" << iv.low.to_string(); + } else { + strm << "@[" << iv_string(iv) << "]"; + } + } else { + strm << "@["; + auto iv_it = acceptable_versions.iter_intervals(); + auto it = iv_it.begin(); + const auto stop = iv_it.end(); + if (it == stop) { + // An empty version range is unsatisfiable. + strm << "⊥"; + } + while (it != stop) { + strm << "(" << iv_string(*it) << ")"; + ++it; + if (it != stop) { + strm << " || "; + } + } + strm << "]"; + } + strm << '/' << joinstr(",", uses | std::views::transform(&bpt::name::str)); + return strm.str(); +} + +dependency dependency::from_data(const json5::data& data) { + dependency ret; + + using namespace semester::walk_ops; + std::vector ver_ranges; + std::vector uses; + + auto parse_version_range = [&](const json5::data& range) { + semver::version low; + semver::version high; + walk(range, + require_mapping{"'versions' elements must be objects"}, + mapping{ + required_key{"low", + "'low' version is required", + require_str{"'low' version must be a string"}, + put_into{low, version_from_string{}}}, + required_key{"high", + "'high' version is required", + require_str{"'high' version must be a string"}, + put_into{high, version_from_string{}}}, + if_key{"_comment", just_accept}, + }); + if (high <= low) { + throw( + semester::walk_error{"'high' version must be strictly greater than 'low' version"}); + } + return semver::range{low, high}; + }; + + walk(data, + require_mapping{"Each dependency should be a JSON object"}, + mapping{ + required_key{"name", + "A string 'name' is required for each dependency", + require_str{"Dependency 'name' must be a string"}, + put_into{ret.name, name_from_string{}}}, + required_key{"versions", + "A 'versions' array is required for each dependency", + require_array{"Dependency 'versions' must be an array"}, + for_each{put_into{std::back_inserter(ver_ranges), parse_version_range}}}, + required_key{"using", + "A dependency 'using' key is required", + require_array{"Dependency 'using' must be an array of usage objects"}, + for_each{require_str{"Each 'using' item must be a usage string"}, + put_into{std::back_inserter(uses), name_from_string{}}}}, + if_key{"_comment", just_accept}, + }); + + if (ver_ranges.empty()) { + throw(semester::walk_error{"A dependency's 'versions' array may not be empty"}); + } + + for (auto& ver : ver_ranges) { + ret.acceptable_versions = ret.acceptable_versions.union_( + pubgrub::interval_set{ver.low(), ver.high()}); + } + + ret.uses = std::move(uses); + + return ret; +} diff --git a/src/bpt/crs/info/dependency.hpp b/src/bpt/crs/info/dependency.hpp new file mode 100644 index 00000000..83c8848e --- /dev/null +++ b/src/bpt/crs/info/dependency.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +#include + +namespace bpt::crs { + +using version_range_set = pubgrub::interval_set; + +struct dependency { + bpt::name name; + version_range_set acceptable_versions; + std::vector uses; + + static dependency from_data(const json5::data&); + + std::string decl_to_string() const noexcept; + + friend void do_repr(auto out, const dependency* self) noexcept { + out.type("bpt::crs::dependency"); + if (self) { + out.value("{}", self->decl_to_string()); + } + } +}; + +} // namespace bpt::crs diff --git a/src/bpt/crs/info/library.cpp b/src/bpt/crs/info/library.cpp new file mode 100644 index 00000000..3b88a19b --- /dev/null +++ b/src/bpt/crs/info/library.cpp @@ -0,0 +1,68 @@ +#include "./library.hpp" + +#include + +using namespace bpt; +using namespace bpt::crs; +using namespace bpt::walk_utils; + +library_info library_info::from_data(const json5::data& data) { + library_info ret; + + using namespace semester::walk_ops; + + walk(data, + require_mapping{"Each library must be a JSON object"}, + mapping{ + required_key{"name", + "A library 'name' string is required", + require_str{"Library 'name' must be a string"}, + put_into{ret.name, name_from_string{}}}, + required_key{"path", + "A library 'path' string is required", + require_str{"Library 'path' must be a string"}, + put_into{ret.path, + [](std::string s) { + auto p = std::filesystem::path(s).lexically_normal(); + if (p.has_root_path()) { + throw semester::walk_error{ + neo:: + ufmt("Library path [{}] must be a relative path", + p.generic_string())}; + } + if (p.begin() != p.end() && *p.begin() == "..") { + throw semester::walk_error{ + neo::ufmt("Library path [{}] must not reach outside " + "of the distribution directory.", + p.generic_string())}; + } + return p; + }}}, + required_key{"using", + "A 'using' array is required", + require_array{"A library's 'using' must be an array of usage objects"}, + for_each{require_str{"Each 'using' element must be a string"}, + put_into{std::back_inserter(ret.intra_using), + name_from_string{}}}}, + required_key{"test-using", + "A 'test-using' array is required", + require_array{ + "A library's 'test-using' must be an array of usage objects"}, + for_each{require_str{"Each 'using' element must be a string"}, + put_into{std::back_inserter(ret.intra_test_using), + name_from_string{}}}}, + required_key{"dependencies", + "A 'dependencies' array is required", + require_array{"'dependencies' must be an array of dependency objects"}, + for_each{put_into{std::back_inserter(ret.dependencies), + dependency::from_data}}}, + required_key{"test-dependencies", + "A 'test-dependencies' array is required", + require_array{ + "'test-dependencies' must be an array of dependency objects"}, + for_each{put_into{std::back_inserter(ret.test_dependencies), + dependency::from_data}}}, + if_key{"_comment", just_accept}, + }); + return ret; +} \ No newline at end of file diff --git a/src/bpt/crs/info/library.hpp b/src/bpt/crs/info/library.hpp new file mode 100644 index 00000000..75f5dd68 --- /dev/null +++ b/src/bpt/crs/info/library.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include "./dependency.hpp" + +#include + +#include + +#include +#include + +namespace bpt::crs { + +struct library_info { + bpt::name name; + std::filesystem::path path; + std::vector intra_using; + std::vector intra_test_using; + std::vector dependencies; + std::vector test_dependencies; + + static library_info from_data(const json5::data& data); + + friend void do_repr(auto out, const library_info* self) noexcept { + out.type("bpt::crs::library_info"); + if (self) { + out.bracket_value( + "name={}, path={}, intra_using={}, intra_test_using={}, dependencies={}, " + "test_dependencies={}", + out.repr_value(self->name), + out.repr_value(self->path), + out.repr_value(self->intra_using), + out.repr_value(self->intra_test_using), + out.repr_value(self->dependencies), + out.repr_value(self->test_dependencies)); + } + } +}; + +} // namespace bpt::crs \ No newline at end of file diff --git a/src/bpt/crs/info/package.cpp b/src/bpt/crs/info/package.cpp new file mode 100644 index 00000000..a5194af0 --- /dev/null +++ b/src/bpt/crs/info/package.cpp @@ -0,0 +1,132 @@ +#include "./package.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace bpt; +using namespace bpt::crs; + +package_info package_info::from_json_str(std::string_view json) { + BPT_E_SCOPE(e_given_meta_json_str{std::string(json)}); + auto data = parse_json_str(json); + return from_json_data(nlohmann_json_as_json5(data)); +} + +package_info package_info::from_json_data(const json5::data& data) { + BPT_E_SCOPE(e_given_meta_json_data{data}); + return bpt_leaf_try { + if (!data.is_object()) { + throw(semester::walk_error{"Root of CRS manifest must be a JSON object"}); + } + auto crs_version = data.as_object().find("schema-version"); + if (crs_version == data.as_object().cend()) { + throw(semester::walk_error{"A 'schema-version' integer is required"}); + } + if (!crs_version->second.is_number() || crs_version->second.as_number() != 0) { + throw(semester::walk_error{"Only 'schema-version' == 0 is supported"}); + } + return from_json_data_v1(data); + } + bpt_leaf_catch(lm::e_invalid_usage_string e)->noreturn_t { + current_error().load(e_invalid_meta_data{neo::ufmt("Invalid usage string '{}'", e.value)}); + throw; + } + bpt_leaf_catch(catch_ e)->noreturn_t { + BOOST_LEAF_THROW_EXCEPTION(e.matched, e_invalid_meta_data{e.matched.what()}); + } + bpt_leaf_catch(const semver::invalid_version& e)->noreturn_t { + BOOST_LEAF_THROW_EXCEPTION( + e_invalid_meta_data{neo::ufmt("Invalid semantic version string '{}'", e.string())}, + BPT_ERR_REF("invalid-version-string")); + } + bpt_leaf_catch(e_name_str invalid_name, invalid_name_reason why)->noreturn_t { + current_error().load(e_invalid_meta_data{neo::ufmt("Invalid name string '{}': {}", + invalid_name.value, + invalid_name_reason_str(why))}); + throw; + }; +} + +std::string package_info::to_json(int indent) const noexcept { + using json = nlohmann::ordered_json; + json ret_libs = json::array(); + auto names_as_str_array = [](neo::ranges::range_of auto&& names) { + auto arr = json::array(); + extend(arr, names | std::views::transform(&bpt::name::str)); + return arr; + }; + auto deps_as_json_array = [this](neo::ranges::range_of auto&& deps) { + json ret = json::array(); + for (auto&& dep : deps) { + json versions = json::array(); + for (auto&& ver : dep.acceptable_versions.iter_intervals()) { + versions.push_back(json::object({ + {"low", ver.low.to_string()}, + {"high", ver.high.to_string()}, + })); + } + json uses = json::array(); + extend(uses, dep.uses | std::views::transform(&bpt::name::str)); + ret.push_back(json::object({ + {"name", dep.name.str}, + {"versions", versions}, + {"using", std::move(uses)}, + })); + } + return ret; + }; + + for (auto&& lib : this->libraries) { + ret_libs.push_back(json::object({ + {"name", lib.name.str}, + {"path", lib.path.generic_string()}, + {"using", std::move(names_as_str_array(lib.intra_using))}, + {"test-using", std::move(names_as_str_array(lib.intra_test_using))}, + {"dependencies", deps_as_json_array(lib.dependencies)}, + {"test-dependencies", deps_as_json_array(lib.test_dependencies)}, + })); + } + json data = json::object({ + {"name", id.name.str}, + {"version", id.version.to_string()}, + {"pkg-version", id.revision}, + {"extra", json5_as_nlohmann_json(extra)}, + {"meta", json5_as_nlohmann_json(meta)}, + {"libraries", std::move(ret_libs)}, + {"schema-version", 0}, + }); + + return indent ? data.dump(indent) : data.dump(); +} + +void package_info::throw_if_invalid() const { + for (auto& lib : libraries) { + auto all_using = ranges::views::concat(lib.intra_using, lib.intra_test_using); + std::ranges::for_each(all_using, [&](auto name) { + if (!ranges::contains(libraries, name, &library_info::name)) { + BOOST_LEAF_THROW_EXCEPTION(e_invalid_meta_data{ + neo::ufmt("Library '{}' uses non-existent sibling library '{}'", + lib.name.str, + name.str)}); + } + }); + } +} diff --git a/src/bpt/crs/info/package.hpp b/src/bpt/crs/info/package.hpp new file mode 100644 index 00000000..e10c4e68 --- /dev/null +++ b/src/bpt/crs/info/package.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include "./library.hpp" +#include "./pkg_id.hpp" + +#include +#include + +#include + +namespace bpt::crs { + +struct e_invalid_meta_data { + std::string value; +}; + +struct e_given_meta_json_str { + std::string value; +}; + +struct e_pkg_json_path { + std::filesystem::path value; +}; + +struct e_given_meta_json_data { + json5::data value; +}; + +struct e_invalid_usage_kind { + std::string value; +}; + +struct package_info { + pkg_id id; + std::vector libraries; + json5::data extra; + json5::data meta; + + static package_info from_json_data_v1(const json5::data&); + static package_info from_json_data(const json5::data&); + static package_info from_json_str(std::string_view json); + + void throw_if_invalid() const; + + std::string to_json(int indent) const noexcept; + std::string to_json() const noexcept { return to_json(0); } + + friend void do_repr(auto out, const package_info* self) noexcept { + out.type("bpt::crs::package_info"); + if (self) { + out.bracket_value("id={}, libraries={}", + self->id.to_string(), + out.repr_value(self->libraries)); + } + } +}; + +} // namespace bpt::crs diff --git a/src/bpt/crs/info/package.test.cpp b/src/bpt/crs/info/package.test.cpp new file mode 100644 index 00000000..ac6ecdeb --- /dev/null +++ b/src/bpt/crs/info/package.test.cpp @@ -0,0 +1,717 @@ +#include "./package.hpp" + +#include +#include +#include +#include + +#include +#include +#include + +#include + +TEST_CASE("Reject bad meta informations") { + auto [given, expect_error] = GENERATE(Catch::Generators::table({ + {"f", + "[json.exception.parse_error.101] parse error at line 1, column 2: syntax error while " + "parsing value - invalid literal; last read: 'f'"}, + {"{\"schema-version\": 0}", "A string 'name' is required"}, + {R"({"schema-version": 0, "name": "foo"})", "A 'version' string is required"}, + {R"({"schema-version": 0, "name": "foo."})", + "Invalid name string 'foo.': Names must not end with a punctuation character"}, + {R"({"schema-version": 0, "name": "foo", "version": "bleh"})", + "Invalid semantic version string 'bleh'"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "schema-version": 0 + })", + "A 'pkg-version' integer is required"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": true, + "schema-version": 0 + })", + "'pkg-version' must be an integer"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 3.14, + "schema-version": 0 + })", + "'pkg-version' must be an integer"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "schema-version": 0 + })", + "A 'libraries' array is required"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": {}, + "schema-version": 0 + })", + "'libraries' must be an array of library objects"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [], + "schema-version": 0 + })", + "'libraries' array must be non-empty"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [12], + "schema-version": 0 + })", + "Each library must be a JSON object"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{}], + "schema-version": 0 + })", + "A library 'name' string is required"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo" + }], + "schema-version": 0 + })", + "A library 'path' string is required"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo", + "path": 12 + }], + "schema-version": 0 + })", + "Library 'path' must be a string"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo", + "path": "/foo/bar" + }], + "schema-version": 0 + })", + "Library path [/foo/bar] must be a relative path"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo", + "path": "../bar" + }], + "schema-version": 0 + })", + "Library path [../bar] must not reach outside of the distribution directory."}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo", + "path": ".", + "test-using": [] + }], + "schema-version": 0 + })", + "A 'using' array is required"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo", + "path": ".", + "using": [] + }], + "schema-version": 0 + })", + "A 'test-using' array is required"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo", + "path": ".", + "test-using": [], + "using": [] + }], + "schema-version": 0 + })", + "A 'dependencies' array is required"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo", + "path": ".", + "using": [], + "dependencies": {} + }], + "schema-version": 0 + })", + "'dependencies' must be an array of dependency objects"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo", + "path": ".", + "using": [], + "dependencies": [12] + }], + "schema-version": 0 + })", + "Each dependency should be a JSON object"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo", + "path": ".", + "test-using": [], + "using": [], + "test-dependencies": [], + "dependencies": [{}] + }], + "schema-version": 0 + })", + "A string 'name' is required for each dependency"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo", + "path": ".", + "test-using": [], + "using": [], + "test-dependencies": [], + "dependencies": [{ + "name": "bad-name." + }] + }], + "schema-version": 0 + })", + "Invalid name string 'bad-name.': Names must not end with a punctuation character"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo", + "path": ".", + "test-using": [], + "using": [], + "dependencies": [{ + "name": "bar", + "versions": [{ + "low": "1.2.3", + "high": "1.2.4" + }], + "using": ["foo"] + }] + }], + "schema-version": 0 + })", + "A 'test-dependencies' array is required"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo", + "path": ".", + "test-using": [], + "using": [], + "test-dependencies": [], + "dependencies": [{ + "name": "bar" + }] + }], + "schema-version": 0 + })", + "A 'versions' array is required for each dependency"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo", + "path": ".", + "test-using": [], + "using": [], + "test-dependencies": [], + "dependencies": [{ + "name": "bar", + "versions": 12 + }] + }], + "schema-version": 0 + })", + "Dependency 'versions' must be an array"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo", + "path": ".", + "test-using": [], + "using": [], + "test-dependencies": [], + "dependencies": [{ + "name": "bar", + "versions": [12] + }] + }], + "schema-version": 0 + })", + "'versions' elements must be objects"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo", + "path": ".", + "test-using": [], + "using": [], + "test-dependencies": [], + "dependencies": [{ + "name": "bar", + "versions": [], + "using": [] + }] + }], + "schema-version": 0 + })", + "A dependency's 'versions' array may not be empty"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo", + "path": ".", + "test-using": [], + "using": [], + "test-dependencies": [], + "dependencies": [{ + "name": "bar", + "versions": [{}] + }] + }], + "schema-version": 0 + })", + "'low' version is required"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo", + "path": ".", + "test-using": [], + "using": [], + "test-dependencies": [], + "dependencies": [{ + "name": "bar", + "versions": [{ + "low": 21 + }] + }] + }], + "schema-version": 0 + })", + "'low' version must be a string"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo", + "path": ".", + "test-using": [], + "using": [], + "test-dependencies": [], + "dependencies": [{ + "name": "bar", + "versions": [{ + "low": "1.2." + }] + }] + }], + "schema-version": 0 + })", + "Invalid semantic version string '1.2.'"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo", + "path": ".", + "test-using": [], + "using": [], + "test-dependencies": [], + "dependencies": [{ + "name": "bar", + "versions": [{ + "low": "1.2.3" + }] + }] + }], + "schema-version": 0 + })", + "'high' version is required"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo", + "path": ".", + "test-using": [], + "using": [], + "test-dependencies": [], + "dependencies": [{ + "name": "bar", + "versions": [{ + "low": "1.2.3", + "high": 12 + }] + }] + }], + "schema-version": 0 + })", + "'high' version must be a string"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo", + "path": ".", + "test-using": [], + "using": [], + "test-dependencies": [], + "dependencies": [{ + "name": "bar", + "versions": [{ + "low": "1.2.3", + "high": "1.2.4" + }] + }] + }], + "schema-version": 0 + })", + "A dependency 'using' key is required"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo", + "path": ".", + "test-using": [], + "using": [], + "test-dependencies": [], + "dependencies": [{ + "name": "bar", + "versions": [{ + "low": "1.2.3", + "high": "1.2.3" + }], + "using": [] + }] + }], + "schema-version": 0 + })", + "'high' version must be strictly greater than 'low' version"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo", + "path": ".", + "test-using": [], + "using": [], + "test-dependencies": [], + "dependencies": [{ + "name": "bar", + "versions": [{ + "low": "1.2.3", + "high": "1.2.3" + }], + "using": [12] + }] + }], + "schema-version": 0 + })", + "Each 'using' item must be a usage string"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo", + "path": ".", + "test-using": [], + "using": [], + "test-dependencies": [], + "dependencies": [{ + "name": "bar", + "versions": [{ + "low": "1.2.3", + "high": "1.2.3" + }], + "using": ["baz/quux"] + }] + }], + "schema-version": 0 + })", + "Invalid name string 'baz/quux'"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo", + "path": ".", + "test-using": [], + "using": [], + "test-dependencies": [], + "dependencies": [{ + "name": "bar", + "versions": [{ + "low": "1.2.3", + "high": "1.2.3" + }], + "using": [] + }] + }] + })", + "A 'schema-version' integer is required"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo", + "path": ".", + "test-using": [], + "using": [], + "test-dependencies": [], + "dependencies": [{ + "name": "bar", + "versions": [{ + "low": "1.2.3", + "high": "1.2.3" + }], + "using": [] + }] + }], + "schema-version": true + })", + "Only 'schema-version' == 0 is supported"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo", + "path": ".", + "test-using": [], + "using": [], + "test-dependencies": [], + "dependencies": [{ + "name": "bar", + "versions": [{ + "low": "1.2.3", + "high": "1.2.3" + }], + "using": [] + }] + }], + "schema-version": 0.4 + })", + "Only 'schema-version' == 0 is supported"}, + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo", + "path": ".", + "test-using": [], + "using": [], + "test-dependencies": [], + "dependencies": [{ + "name": "bar", + "versions": [{ + "low": "1.2.3", + "high": "1.2.3" + }], + "using": [] + }] + }], + "schema-version": 3 + })", + "Only 'schema-version' == 0 is supported"}, + })); + INFO("Parsing data: " << given); + CAPTURE(expect_error); + bpt_leaf_try { + bpt::crs::package_info::from_json_str(given); + FAIL("Expected a failure, but no failure occurred"); + } + bpt_leaf_catch(bpt::e_json_parse_error err, + bpt::crs::e_given_meta_json_str const* json_str, + boost::leaf::verbose_diagnostic_info const& diag_info) { + CAPTURE(diag_info); + CHECKED_IF(json_str) { CHECK(json_str->value == given); } + CHECK_THAT(err.value, Catch::Matchers::Contains(expect_error)); + } + bpt_leaf_catch(bpt::crs::e_given_meta_json_str const* error_str, + bpt::crs::e_given_meta_json_data const* error_data, + bpt::crs::e_invalid_meta_data e, + boost::leaf::verbose_diagnostic_info const& diag_info) { + CAPTURE(diag_info); + CHECKED_IF(error_str) { CHECK(error_str->value == given); } + CHECK(error_data); + CHECK_THAT(e.value, Catch::Matchers::Contains(expect_error)); + } + bpt_leaf_catch_all { // + FAIL_CHECK("Unexpected error: " << diagnostic_info); + }; +} + +TEST_CASE("Check some valid meta JSON") { + const auto given = GENERATE(R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo", + "path": ".", + "test-using": [], + "using": [], + "test-dependencies": [], + "dependencies": [] + }], + "schema-version": 0 + })"); + REQUIRE_NOTHROW(bpt::crs::package_info::from_json_str(given)); +} + +auto mk_name = [](std::string_view s) { return bpt::name::from_string(s).value(); }; + +#ifndef _MSC_VER // MSVC struggles with compiling this test +TEST_CASE("Check parse results") { + using pkg_meta = bpt::crs::package_info; + using lib_meta = bpt::crs::library_info; + using dependency = bpt::crs::dependency; + const auto [given, expect] = GENERATE(Catch::Generators::table({ + {R"({ + "name": "foo", + "version": "1.2.3", + "pkg-version": 1, + "libraries": [{ + "name": "foo", + "path": ".", + "test-using": [], + "using": [], + "test-dependencies": [], + "dependencies": [{ + "name": "bar", + "versions": [{ + "low": "1.0.0", + "high": "1.5.1" + }], + "using": ["bar"] + }], + "test-dependencies": [] + }], + "schema-version": 0 + })", + pkg_meta{ + .id=bpt::crs::pkg_id{ + .name = mk_name("foo"), + .version = semver::version::parse("1.2.3"), + .revision = 1, + }, + .libraries = {lib_meta{ + .name = mk_name("foo"), + .path = ".", + .intra_using = {}, + .intra_test_using = {}, + .dependencies = {dependency{ + .name = mk_name("bar"), + .acceptable_versions + = bpt::crs::version_range_set{semver::version::parse("1.0.0"), + semver::version::parse("1.5.1")}, + .uses = {bpt::name{"baz"}}, + }}, + .test_dependencies ={}, + }}, + .extra = {}, + .meta = {}, + }}, + })); + + auto meta = bpt_leaf_try { return bpt::crs::package_info::from_json_str(given); } + bpt_leaf_catch_all->bpt::noreturn_t { + FAIL("Unexpected error: " << diagnostic_info); + std::terminate(); + }; + + CHECK(meta.id == expect.id); + CHECK(meta.extra == expect.extra); + CHECKED_IF(meta.libraries.size() == expect.libraries.size()) { + auto res_lib_it = meta.libraries.cbegin(); + auto exp_lib_it = expect.libraries.cbegin(); + for (; res_lib_it != meta.libraries.cend(); ++res_lib_it, ++exp_lib_it) { + CHECK(res_lib_it->name == exp_lib_it->name); + CHECK(res_lib_it->path == exp_lib_it->path); + CHECKED_IF(res_lib_it->dependencies.size() == exp_lib_it->dependencies.size()) { + auto res_dep_it = res_lib_it->dependencies.cbegin(); + auto exp_dep_it = res_lib_it->dependencies.cbegin(); + for (; res_dep_it != res_lib_it->dependencies.cbegin(); + ++res_dep_it, ++exp_dep_it) { + CHECK(res_dep_it->name == exp_dep_it->name); + CHECK(res_dep_it->acceptable_versions == exp_dep_it->acceptable_versions); + CHECK(res_dep_it->uses == exp_dep_it->uses); + } + } + } + } + CHECK_NOTHROW(meta.to_json()); +} +#endif // _MSC_VER \ No newline at end of file diff --git a/src/bpt/crs/info/package.walk.v0.cpp b/src/bpt/crs/info/package.walk.v0.cpp new file mode 100644 index 00000000..88834a46 --- /dev/null +++ b/src/bpt/crs/info/package.walk.v0.cpp @@ -0,0 +1,71 @@ +#include "./package.hpp" + +#include +#include +#include + +#include + +#include + +using namespace bpt; +using namespace bpt::crs; +using namespace bpt::walk_utils; + +namespace { + +auto require_integer_key(std::string name) { + using namespace semester::walk_ops; + return [name](const json5::data& dat) { + if (!dat.is_number()) { + return walk.reject(neo::ufmt("'{}' must be an integer", name)); + } + double d = dat.as_number(); + if (d != (double)(int)d) { + return walk.reject(neo::ufmt("'{}' must be an integer", name)); + } + return walk.pass; + }; +} + +package_info meta_from_data(const json5::data& data) { + package_info ret; + using namespace semester::walk_ops; + + walk(data, + require_mapping{"Root of CRS manifest must be a JSON object"}, + mapping{ + if_key{"$schema", just_accept}, + required_key{"name", + "A string 'name' is required", + require_str{"'name' must be a string"}, + put_into{ret.id.name, name_from_string{}}}, + required_key{"version", + "A 'version' string is required", + require_str{"'version' must be a string"}, + put_into{ret.id.version, version_from_string{}}}, + required_key{"pkg-version", + "A 'pkg-version' integer is required", + require_integer_key("pkg-version"), + put_into{ret.id.revision, [](double d) { return int(d); }}}, + required_key{"libraries", + "A 'libraries' array is required", + require_array{"'libraries' must be an array of library objects"}, + for_each{put_into{std::back_inserter(ret.libraries), + library_info::from_data}}}, + required_key{"schema-version", "A 'schema-version' number is required", just_accept}, + if_key{"extra", put_into{ret.extra}}, + if_key{"meta", put_into{ret.meta}}, + if_key{"_comment", just_accept}, + }); + + if (ret.libraries.empty()) { + throw semester::walk_error{"'libraries' array must be non-empty"}; + } + + return ret; +} + +} // namespace + +package_info package_info::from_json_data_v1(const json5::data& dat) { return meta_from_data(dat); } diff --git a/src/bpt/crs/info/pkg_id.cpp b/src/bpt/crs/info/pkg_id.cpp new file mode 100644 index 00000000..41037346 --- /dev/null +++ b/src/bpt/crs/info/pkg_id.cpp @@ -0,0 +1,59 @@ +#include "./pkg_id.hpp" + +#include +#include +#include +#include +#include + +#include + +#include +#include + +using namespace bpt; +using namespace bpt::crs; + +crs::pkg_id crs::pkg_id::parse(const std::string_view sv) { + BPT_E_SCOPE(e_invalid_pkg_id_str{std::string(sv)}); + auto at_pos = sv.find("@"); + if (at_pos == sv.npos) { + BOOST_LEAF_THROW_EXCEPTION(e_human_message{ + "Package ID must contain an '@' character between the package name and the version"}); + } + + auto name_sv = sv.substr(0, at_pos); + auto name = *bpt::name::from_string(name_sv); + + auto ver_str = sv.substr(at_pos + 1); + + auto tilde_pos = ver_str.find("~"); + int pkg_version = 0; + if (tilde_pos != ver_str.npos) { + auto tail = ver_str.substr(tilde_pos + 1); + auto fc_res = std::from_chars(tail.data(), tail.data() + tail.size(), pkg_version); + if (fc_res.ec != std::errc{}) { + BOOST_LEAF_THROW_EXCEPTION( + e_human_message{"Invalid pkg-version integer suffix in package ID string"}); + } + ver_str = ver_str.substr(0, tilde_pos); + } + try { + auto version = semver::version::parse(ver_str); + return crs::pkg_id{name, version, pkg_version}; + } catch (const semver::invalid_version& exc) { + BOOST_LEAF_THROW_EXCEPTION(exc, BPT_ERR_REF("invalid-version-string")); + } +} + +std::string crs::pkg_id::to_string() const noexcept { + return neo::ufmt("{}@{}~{}", name.str, version.to_string(), revision); +} + +static auto _tie(const crs::pkg_id& pid) noexcept { + return std::tie(pid.name, pid.version, pid.revision); +} + +bool crs::pkg_id::operator<(const crs::pkg_id& other) const noexcept { + return _tie(*this) < _tie(other); +} diff --git a/src/bpt/crs/info/pkg_id.hpp b/src/bpt/crs/info/pkg_id.hpp new file mode 100644 index 00000000..060d84ef --- /dev/null +++ b/src/bpt/crs/info/pkg_id.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +namespace bpt::crs { + +struct e_invalid_pkg_id_str { + std::string value; +}; + +struct pkg_id { + /// The name of the package + bpt::name name; + /// The version of the code in the package package + semver::version version; + /// The revision of the package itself + int revision; + + /// Parse the given string as a package-id string + static pkg_id parse(std::string_view sv); + + /// Convert the package ID back into a string + std::string to_string() const noexcept; + + bool operator<(const pkg_id&) const noexcept; + bool operator==(const pkg_id&) const noexcept = default; +}; + +struct e_no_such_pkg { + pkg_id value; +}; + +} // namespace bpt::crs diff --git a/src/dds/pkg/id.test.cpp b/src/bpt/crs/info/pkg_id.test.cpp similarity index 75% rename from src/dds/pkg/id.test.cpp rename to src/bpt/crs/info/pkg_id.test.cpp index 13bdd954..810917b6 100644 --- a/src/dds/pkg/id.test.cpp +++ b/src/bpt/crs/info/pkg_id.test.cpp @@ -1,7 +1,12 @@ -#include +#include "./pkg_id.hpp" #include +TEST_CASE("Parse a pkg_id string") { + auto pid = bpt::crs::pkg_id::parse("foo@1.2.3"); + CHECK(pid.name.str == "foo"); +} + TEST_CASE("Package package ID strings") { struct case_ { std::string_view string; @@ -9,14 +14,14 @@ TEST_CASE("Package package ID strings") { std::string_view expect_version; }; auto [id_str, exp_name, exp_ver] = GENERATE(Catch::Generators::values({ - {"foo@1.2.3", "foo", "1.2.3"}, - {"foo@1.2.3-beta", "foo", "1.2.3-beta"}, - {"foo@1.2.3-alpha", "foo", "1.2.3-alpha"}, + {"foo@1.2.3~0", "foo", "1.2.3"}, + {"foo@1.2.3-beta~0", "foo", "1.2.3-beta"}, + {"foo@1.2.3-alpha~0", "foo", "1.2.3-alpha"}, })); - auto pk_id = dds::pkg_id::parse(id_str); + auto pk_id = bpt::crs::pkg_id::parse(id_str); CHECK(pk_id.to_string() == id_str); - CHECK(pk_id.name == exp_name); + CHECK(pk_id.name.str == exp_name); CHECK(pk_id.version.to_string() == exp_ver); } @@ -44,8 +49,8 @@ TEST_CASE("Package ordering") { {"foo@0.1.2-alpha", less_than, "foo@1.0.0"}, })); - auto lhs = dds::pkg_id::parse(lhs_str); - auto rhs = dds::pkg_id::parse(rhs_str); + auto lhs = bpt::crs::pkg_id::parse(lhs_str); + auto rhs = bpt::crs::pkg_id::parse(rhs_str); if (ord == less_than) { CHECK(lhs < rhs); diff --git a/src/bpt/crs/remote.cpp b/src/bpt/crs/remote.cpp new file mode 100644 index 00000000..673ff077 --- /dev/null +++ b/src/bpt/crs/remote.cpp @@ -0,0 +1,88 @@ +#include "./remote.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +using namespace bpt; +using namespace bpt::crs; + +namespace { + +neo::url calc_pkg_url(neo::url_view from, pkg_id pkg) { + return from.normalized() / "pkg" / pkg.name.str + / neo::ufmt("{}~{}", pkg.version.to_string(), pkg.revision) / "pkg.tgz"; +} + +void expand_tgz(path_ref tgz_path, path_ref into) { + auto infile = bpt::open_file(tgz_path, std::ios::binary | std::ios::in); + fs::create_directories(into); + neo::expand_directory_targz( + neo::expand_options{ + .destination_directory = into, + .input_name = tgz_path.string(), + }, + infile); +} + +} // namespace + +void crs::pull_pkg_ar_from_remote(path_ref dest, neo::url_view from, pkg_id pkg) { + bpt_log(trace, + "Pulling package archive from remote {} for {} to {}", + from.to_string(), + pkg.to_string(), + dest.string()); + auto tgz_url = calc_pkg_url(from, pkg); + + fs::create_directories(dest.parent_path()); + + if (from.scheme == "file") { + auto local_path = fs::path{tgz_url.path}; + bpt::copy_file(local_path, dest).value(); + return; + } + + auto tmp = dest.parent_path() / ".bpt-download.tmp"; + neo_defer { std::ignore = ensure_absent(tmp); }; + + { + auto& pool = http_pool::thread_local_pool(); + auto reqres = pool.request(tgz_url); + reqres.save_file(tmp); + } + + bpt::ensure_absent(dest).value(); + bpt::move_file(tmp, dest).value(); +} + +void crs::pull_pkg_from_remote(path_ref expand_into, neo::url_view from, pkg_id pkg) { + fs::path expand_tgz_path; + if (from.scheme == "file") { + bpt_log(debug, + "Expanding package archive of {} from remote [{}] in-place into [{}]", + pkg.to_string(), + from.to_string(), + expand_into.string()); + // We can skip copying the tarball and just expand the one in the repository directly. + fs::path tgz_path = calc_pkg_url(from, pkg).path; + expand_tgz(tgz_path, expand_into); + } else { + // Create a tempdir, download into it, and expand that + auto tmpdir = bpt::temporary_dir::create_in(expand_into.parent_path()); + auto tgz_path = tmpdir.path() / neo::ufmt("~{}.tgz", pkg.to_string()); + // Download: + pull_pkg_ar_from_remote(tgz_path, from, pkg); + // Expand: + expand_tgz(tgz_path, expand_into); + } +} diff --git a/src/bpt/crs/remote.hpp b/src/bpt/crs/remote.hpp new file mode 100644 index 00000000..a8dd3293 --- /dev/null +++ b/src/bpt/crs/remote.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include "./info/pkg_id.hpp" + +#include +#include + +namespace bpt::crs { + +void pull_pkg_ar_from_remote(path_ref dest, neo::url_view from, pkg_id pkg); +void pull_pkg_from_remote(path_ref expand_into, neo::url_view from, pkg_id pkg); + +} // namespace bpt::crs diff --git a/src/bpt/crs/repo.cpp b/src/bpt/crs/repo.cpp new file mode 100644 index 00000000..0e83f297 --- /dev/null +++ b/src/bpt/crs/repo.cpp @@ -0,0 +1,240 @@ +#include "./repo.hpp" + +#include "./error.hpp" + +#include +#include +#include +#include +#include +#include +#include // for e_nonesuch_package +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace bpt; +using namespace bpt::crs; +using namespace neo::sqlite3::literals; + +using path_ref = const std::filesystem::path&; + +namespace { + +void ensure_migrated(unique_database& db) { + apply_db_migrations(db, "crs_repo_meta", [](unique_database& db) { + db.exec_script(R"( + CREATE TABLE crs_repo_self ( + rowid INTEGER PRIMARY KEY, + name TEXT NOT NULL + ); + + CREATE TABLE crs_repo_packages ( + package_id INTEGER PRIMARY KEY, + meta_json TEXT NOT NULL, + name TEXT NOT NULL + GENERATED ALWAYS AS (json_extract(meta_json, '$.name')) + VIRTUAL, + version TEXT NOT NULL + GENERATED ALWAYS AS (json_extract(meta_json, '$.version')) + VIRTUAL, + pkg_version INTEGER NOT NULL + GENERATED ALWAYS AS (json_extract(meta_json, '$.pkg-version')) + VIRTUAL, + UNIQUE(name, version, pkg_version) + ); + )"_sql); + }).value(); +} + +void copy_source_tree(path_ref from_dir, path_ref to_dir) { + bpt_log(debug, "Copy source tree [{}] -> [{}]", from_dir.string(), to_dir.string()); + fs::create_directories(to_dir.parent_path()); + bpt::copy_tree(from_dir, to_dir, fs::copy_options::recursive).value(); +} + +void copy_library(path_ref lib_root, path_ref to_root) { + auto src_dir = lib_root / "src"; + if (fs::is_directory(src_dir)) { + copy_source_tree(src_dir, to_root / "src"); + } + auto inc_dir = lib_root / "include"; + if (fs::is_directory(inc_dir)) { + copy_source_tree(inc_dir, to_root / "include"); + } +} + +void archive_package_libraries(path_ref from_dir, path_ref dest_root, const package_info& pkg) { + for (auto& lib : pkg.libraries) { + fs::path relpath = lib.path; + auto dest = dest_root / relpath; + auto src = from_dir / relpath; + copy_library(src, dest); + } +} + +} // namespace + +void repository::_vacuum_and_compress() const { + neo_assert(invariant, + !_db.sqlite3_db().is_transaction_active(), + "Database cannot be recompressed while a transaction is open"); + db_exec(_prepare("VACUUM"_sql)).value(); + bpt::compress_file_gz(_dirpath / "repo.db", _dirpath / "repo.db.gz").value(); +} + +repository repository::create(path_ref dirpath, std::string_view name) { + BPT_E_SCOPE(e_repo_open_path{dirpath}); + std::filesystem::create_directories(dirpath); + auto db = unique_database::open((dirpath / "repo.db").string()).value(); + ensure_migrated(db); + bpt_leaf_try { + db_exec(db.prepare("INSERT INTO crs_repo_self (rowid, name) VALUES (1729, ?)"_sql), name) + .value(); + } + bpt_leaf_catch(matchv) { + BOOST_LEAF_THROW_EXCEPTION(current_error(), e_repo_already_init{}); + }; + auto r = repository{std::move(db), dirpath}; + r._vacuum_and_compress(); + return r; +} + +repository repository::open_existing(path_ref dirpath) { + BPT_E_SCOPE(e_repo_open_path{dirpath}); + auto db = unique_database::open_existing((dirpath / "repo.db").string()).value(); + ensure_migrated(db); + return repository{std::move(db), dirpath}; +} + +neo::sqlite3::statement& repository::_prepare(neo::sqlite3::sql_string_literal sql) const { + return _db.prepare(sql); +} + +std::string repository::name() const { + return db_cell(_prepare("SELECT name FROM crs_repo_self WHERE rowid=1729"_sql)) + .value(); +} + +fs::path repository::subdir_of(const package_info& pkg) const noexcept { + return this->pkg_dir() / pkg.id.name.str + / neo::ufmt("{}~{}", pkg.id.version.to_string(), pkg.id.revision); +} + +void repository::import_dir(path_ref dirpath) { + BPT_E_SCOPE(e_repo_importing_dir{dirpath}); + auto sd = sdist::from_directory(dirpath); + auto& pkg = sd.pkg; + BPT_E_SCOPE(e_repo_importing_package{pkg}); + auto dest_dir = subdir_of(pkg); + fs::create_directories(dest_dir); + + // Copy the package into a temporary directory + auto prep_dir = bpt::temporary_dir::create_in(dest_dir); + archive_package_libraries(dirpath, prep_dir.path(), pkg); + fs::create_directories(prep_dir.path()); + bpt::write_file(prep_dir.path() / "pkg.json", pkg.to_json(2)); + + auto tmp_tgz = pkg_dir() / (prep_dir.path().filename().string() + ".tgz"); + neo::compress_directory_targz(prep_dir.path(), tmp_tgz); + neo_defer { std::ignore = ensure_absent(tmp_tgz); }; + + if (pkg.id.revision < 1) { + BOOST_LEAF_THROW_EXCEPTION(e_repo_import_invalid_pkg_version{ + "Package pkg-version must be a positive non-zero integer in order to be imported into " + "a repository"}); + } + + neo::sqlite3::transaction_guard tr{_db.sqlite3_db()}; + bpt_leaf_try { + db_exec( // + _prepare(R"( + INSERT INTO crs_repo_packages (meta_json) + VALUES (?) + )"_sql), + std::string_view(pkg.to_json())) + .value(); + } + bpt_leaf_catch(matchv) { + BOOST_LEAF_THROW_EXCEPTION(current_error(), e_repo_import_pkg_already_present{}); + }; + + move_file(tmp_tgz, dest_dir / "pkg.tgz").value(); + bpt::copy_file(prep_dir.path() / "pkg.json", dest_dir / "pkg.json").value(); + tr.commit(); + _vacuum_and_compress(); + + NEO_EMIT(ev_repo_imported_package{*this, dirpath, pkg}); +} + +neo::any_input_range repository::all_packages() const { + auto& q = _prepare(R"( + SELECT meta_json + FROM crs_repo_packages AS this + ORDER BY package_id + )"_sql); + auto rst = neo::copy_shared(q.auto_reset()); + return db_query(q) + | std::views::transform([pin = rst](auto tup) -> package_info { + auto [json_str] = tup; + return package_info::from_json_str(json_str); + }); +} + +neo::any_input_range repository::all_latest_rev_packages() const { + auto& q = _prepare(R"( + SELECT meta_json + FROM crs_repo_packages AS this + WHERE NOT EXISTS( + SELECT 1 FROM crs_repo_packages AS other + WHERE other.pkg_version > this.pkg_version + AND other.version = this.version + AND other.name = this.name + ) + ORDER BY package_id + )"_sql); + auto rst = neo::copy_shared(q.auto_reset()); + return db_query(q) + | std::views::transform([pin = rst](auto tup) -> package_info { + auto [json_str] = tup; + return package_info::from_json_str(json_str); + }); +} + +void repository::remove_pkg(const package_info& meta) { + auto to_delete = subdir_of(meta); + auto rows = neo::sqlite3::exec_rows(_prepare(R"( + DELETE FROM crs_repo_packages + WHERE name = ?1 + AND version = ?2 + AND (pkg_version = ?3) + RETURNING 1 + )"_sql), + meta.id.name.str, + meta.id.version.to_string(), + meta.id.revision) + .value(); + auto n_deleted = std::ranges::distance(rows); + if (n_deleted == 0) { + auto req_id = meta.id.to_string(); + auto all_ids = this->all_packages() | neo::lref + | std::views::transform([](auto inf) { return inf.id.to_string(); }) | neo::to_vector; + BOOST_LEAF_THROW_EXCEPTION(bpt::e_nonesuch_package{req_id, did_you_mean(req_id, all_ids)}); + } + bpt_log(debug, "Deleting subdirectory [{}]", to_delete.string()); + ensure_absent(to_delete).value(); +} diff --git a/src/bpt/crs/repo.hpp b/src/bpt/crs/repo.hpp new file mode 100644 index 00000000..a9ed3a4b --- /dev/null +++ b/src/bpt/crs/repo.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include "./info/package.hpp" + +#include + +#include + +#include +#include +#include + +namespace bpt::crs { + +class repository { + unique_database _db; + std::filesystem::path _dirpath; + + neo::sqlite3::statement& _prepare(neo::sqlite3::sql_string_literal) const; + + explicit repository(unique_database&& db, const std::filesystem::path& dirpath) noexcept + : _db(std::move(db)) + , _dirpath(dirpath) {} + + void _vacuum_and_compress() const; + +public: + static repository create(const std::filesystem::path& directory, std::string_view name); + static repository open_existing(const std::filesystem::path& directory); + + std::filesystem::path subdir_of(const package_info&) const noexcept; + + auto pkg_dir() const noexcept { return _dirpath / "pkg"; } + auto& root() const noexcept { return _dirpath; } + std::string name() const; + + void import_targz(const std::filesystem::path& tgz_path); + void import_dir(const std::filesystem::path& dirpath); + + void remove_pkg(const package_info&); + + neo::any_input_range all_packages() const; + neo::any_input_range all_latest_rev_packages() const; +}; + +struct ev_repo_imported_package { + repository const& into_repo; + std::filesystem::path const& from_path; + package_info const& pkg_meta; +}; + +} // namespace bpt::crs \ No newline at end of file diff --git a/src/bpt/crs/repo.test.cpp b/src/bpt/crs/repo.test.cpp new file mode 100644 index 00000000..50433d15 --- /dev/null +++ b/src/bpt/crs/repo.test.cpp @@ -0,0 +1,66 @@ +#include "./repo.hpp" + +#include +#include +#include +#include + +#include +#include + +#include + +namespace fs = std::filesystem; + +TEST_CASE("Init repo") { + auto tempdir = bpt::temporary_dir::create(); + auto repo + = REQUIRES_LEAF_NOFAIL(bpt::crs::repository::create(tempdir.path(), "simple-test-repo")); + bpt_leaf_try { + bpt::crs::repository::create(repo.root(), "test"); + FAIL("Expected an error, but no error occurred"); + } + bpt_leaf_catch(bpt::crs::e_repo_already_init) {} + bpt_leaf_catch_all { FAIL("Unexpected failure: " << diagnostic_info); }; + CHECK(repo.name() == "simple-test-repo"); +} + +struct empty_repo { + bpt::temporary_dir tempdir = bpt::temporary_dir::create(); + bpt::crs::repository repo = bpt::crs::repository::create(tempdir.path(), "test"); +}; + +TEST_CASE_METHOD(empty_repo, "Import a simple packages") { + REQUIRES_LEAF_NOFAIL(repo.import_dir(bpt::testing::DATA_DIR / "simple.crs")); + auto all = REQUIRES_LEAF_NOFAIL(repo.all_packages() | neo::to_vector); + REQUIRE(all.size() == 1); + auto first = all.front(); + CHECK(first.id.name.str == "test-pkg"); + CHECK(first.id.version.to_string() == "1.2.43"); + CHECKED_IF(fs::is_directory(repo.pkg_dir())) { + CHECKED_IF(fs::is_directory(repo.pkg_dir() / "test-pkg")) { + CHECKED_IF(fs::is_directory(repo.pkg_dir() / "test-pkg/1.2.43~1")) { + CHECK(fs::is_regular_file(repo.pkg_dir() / "test-pkg/1.2.43~1/pkg.tgz")); + CHECK(fs::is_regular_file(repo.pkg_dir() / "test-pkg/1.2.43~1/pkg.json")); + } + } + } + + REQUIRES_LEAF_NOFAIL(repo.import_dir(bpt::testing::DATA_DIR / "simple2.crs")); + all = REQUIRES_LEAF_NOFAIL(repo.all_packages() | neo::to_vector); + REQUIRE(all.size() == 2); + first = all[0]; + auto second = all[1]; + CHECK(first.id.name.str == "test-pkg"); + CHECK(second.id.name.str == "test-pkg"); + CHECK(first.id.version.to_string() == "1.2.43"); + CHECK(second.id.version.to_string() == "1.3.0"); + + REQUIRES_LEAF_NOFAIL(repo.import_dir(bpt::testing::DATA_DIR / "simple3.crs")); + all = REQUIRES_LEAF_NOFAIL(repo.all_packages() | neo::to_vector); + REQUIRE(all.size() == 3); + auto third = all[2]; + CHECK(third.id.name.str == "test-pkg"); + CHECK(third.id.version.to_string() == "1.3.0"); + CHECK(third.id.revision == 2); +} diff --git a/src/dds/db/database.cpp b/src/bpt/db/database.cpp similarity index 56% rename from src/dds/db/database.cpp rename to src/bpt/db/database.cpp index 8f782cb1..107a5bd6 100644 --- a/src/dds/db/database.cpp +++ b/src/bpt/db/database.cpp @@ -1,18 +1,20 @@ #include "./database.hpp" -#include -#include +#include +#include +#include +#include #include #include -#include +#include #include #include #include #include -using namespace dds; +using namespace bpt; namespace nsql = neo::sqlite3; using nsql::exec; @@ -21,84 +23,87 @@ using namespace std::literals; namespace { -void migrate_1(nsql::database& db) { +void migrate_1(nsql::connection& db) { db.exec(R"( - DROP TABLE IF EXISTS dds_deps; - DROP TABLE IF EXISTS dds_file_commands; - DROP TABLE IF EXISTS dds_files; - DROP TABLE IF EXISTS dds_compile_deps; - DROP TABLE IF EXISTS dds_compilations; - DROP TABLE IF EXISTS dds_source_files; - CREATE TABLE dds_source_files ( + DROP TABLE IF EXISTS bpt_deps; + DROP TABLE IF EXISTS bpt_file_commands; + DROP TABLE IF EXISTS bpt_files; + DROP TABLE IF EXISTS bpt_compile_deps; + DROP TABLE IF EXISTS bpt_compilations; + DROP TABLE IF EXISTS bpt_source_files; + CREATE TABLE bpt_source_files ( file_id INTEGER PRIMARY KEY, path TEXT NOT NULL UNIQUE ); - CREATE TABLE dds_compilations ( + CREATE TABLE bpt_compilations ( compile_id INTEGER PRIMARY KEY, file_id INTEGER NOT NULL - UNIQUE REFERENCES dds_source_files(file_id), + UNIQUE REFERENCES bpt_source_files(file_id), command TEXT NOT NULL, output TEXT NOT NULL, + toolchain_hash INTEGER NOT NULL, n_compilations INTEGER NOT NULL DEFAULT 0, avg_duration INTEGER NOT NULL DEFAULT 0 ); - CREATE TABLE dds_compile_deps ( + CREATE TABLE bpt_compile_deps ( input_file_id INTEGER NOT NULL - REFERENCES dds_source_files(file_id), + REFERENCES bpt_source_files(file_id), output_file_id INTEGER NOT NULL - REFERENCES dds_source_files(file_id), + REFERENCES bpt_source_files(file_id), input_mtime INTEGER NOT NULL, UNIQUE(input_file_id, output_file_id) ); - )"); + )") + .throw_if_error(); } -void ensure_migrated(nsql::database& db) { +void ensure_migrated(nsql::connection& db) { db.exec(R"( PRAGMA foreign_keys = 1; - DROP TABLE IF EXISTS dds_meta; - CREATE TABLE IF NOT EXISTS dds_meta_1 AS - WITH init (version) AS (VALUES ('eggs')) + DROP TABLE IF EXISTS bpt_meta; + CREATE TABLE IF NOT EXISTS bpt_meta_1 AS + WITH init (version) AS (VALUES ('')) SELECT * FROM init; - )"); + )") + .throw_if_error(); nsql::transaction_guard tr{db}; - auto version_st = db.prepare("SELECT version FROM dds_meta_1"); - auto [version_str] = nsql::unpack_single(version_st); + auto version_st = *db.prepare("SELECT version FROM bpt_meta_1"); + auto version_str = *nsql::one_cell(version_st); - const auto cur_version = "alpha-5"sv; + const auto cur_version = "alpha-5-dev1"sv; if (cur_version != version_str) { if (!version_str.empty()) { - dds_log(info, "NOTE: A prior version of the project build database was found."); - dds_log(info, "This is not an error, but incremental builds will be invalidated."); - dds_log(info, "The database is being upgraded, and no further action is necessary."); + bpt_log(info, "NOTE: A prior version of the project build database was found."); + bpt_log(info, "This is not an error, but incremental builds will be invalidated."); + bpt_log(info, "The database is being upgraded, and no further action is necessary."); } migrate_1(db); } - exec(db.prepare("UPDATE dds_meta_1 SET version=?"), cur_version); + nsql::exec(*db.prepare("UPDATE bpt_meta_1 SET version=?"), cur_version).throw_if_error(); } } // namespace database database::open(const std::string& db_path) { - auto db = nsql::database::open(db_path); + auto db = *nsql::connection::open(db_path); try { ensure_migrated(db); - } catch (const nsql::sqlite3_error& e) { - dds_log( + } catch (const nsql::error& e) { + bpt_log( error, "Failed to load the databsae. It appears to be invalid/corrupted. We'll delete it and " "create a new one. The exception message is: {}", e.what()); fs::remove(db_path); - db = nsql::database::open(db_path); + db = *nsql::connection::open(db_path); try { ensure_migrated(db); - } catch (const nsql::sqlite3_error& e) { - dds_log(critical, + } catch (const nsql::error& e) { + bpt_log(critical, "Failed to apply database migrations to recovery database. This is a critical " "error. The exception message is: {}", e.what()); @@ -108,47 +113,49 @@ database database::open(const std::string& db_path) { return database(std::move(db)); } -database::database(nsql::database db) +database::database(nsql::connection db) : _db(std::move(db)) {} std::int64_t database::_record_file(path_ref path_) { - auto path = fs::weakly_canonical(path_); - nsql::exec(_stmt_cache(R"( - INSERT OR IGNORE INTO dds_source_files (path) - VALUES (?) - )"_sql), - path.generic_string()); - auto& st = _stmt_cache(R"( - SELECT file_id - FROM dds_source_files - WHERE path = ?1 + auto path = bpt::normalize_path(path_); + + auto found = _stored_file_ids_cache.find(path); + if (found != _stored_file_ids_cache.end()) { + return found->second; + } + auto& st = _stmt_cache(R"( + INSERT INTO bpt_source_files (path) + VALUES (?1) + ON CONFLICT (path) DO UPDATE SET path=path + RETURNING file_id )"_sql); - st.reset(); - auto str = path.generic_string(); - st.bindings()[1] = str; - auto [rowid] = nsql::unpack_single(st); - return rowid; + auto [fid] = *nsql::one_row(st, path.generic_string()); + _stored_file_ids_cache.emplace(path, fid); + return fid; } void database::record_dep(path_ref input, path_ref output, fs::file_time_type input_mtime) { auto in_id = _record_file(input); auto out_id = _record_file(output); auto& st = _stmt_cache(R"( - INSERT OR REPLACE INTO dds_compile_deps (input_file_id, output_file_id, input_mtime) + INSERT OR REPLACE INTO bpt_compile_deps (input_file_id, output_file_id, input_mtime) VALUES (?, ?, ?) )"_sql); - nsql::exec(st, in_id, out_id, input_mtime.time_since_epoch().count()); + nsql::exec(st, in_id, out_id, input_mtime.time_since_epoch().count()).throw_if_error(); } void database::record_compilation(path_ref file, const completed_compilation& cmd) { auto file_id = _record_file(file); auto& st = _stmt_cache(R"( - INSERT INTO dds_compilations(file_id, command, output, n_compilations, avg_duration) - VALUES (:file_id, :command, :output, 1, :duration) + INSERT INTO bpt_compilations + (file_id, command, output, n_compilations, toolchain_hash, avg_duration) + VALUES + (:file_id, :command, :output, 1, :toolchain_hash, :duration) ON CONFLICT(file_id) DO UPDATE SET - command = ?2, - output = ?3, + command = :command, + output = :output, + toolchain_hash = :toolchain_hash, n_compilations = CASE WHEN :duration < 500 THEN n_compilations ELSE min(10, n_compilations + 1) @@ -162,20 +169,22 @@ void database::record_compilation(path_ref file, const completed_compilation& cm file_id, std::string_view(cmd.quoted_command), std::string_view(cmd.output), - cmd.duration.count()); + cmd.toolchain_hash, + cmd.duration.count()) + .throw_if_error(); } void database::forget_inputs_of(path_ref file) { auto& st = _stmt_cache(R"( WITH id_to_delete AS ( SELECT file_id - FROM dds_source_files + FROM bpt_source_files WHERE path = ? ) - DELETE FROM dds_compile_deps + DELETE FROM bpt_compile_deps WHERE output_file_id IN id_to_delete )"_sql); - nsql::exec(st, fs::weakly_canonical(file).generic_string()); + nsql::exec(st, fs::weakly_canonical(file).generic_string()).throw_if_error(); } std::optional> database::inputs_of(path_ref file_) const { @@ -183,12 +192,12 @@ std::optional> database::inputs_of(path_ref file_) auto& st = _stmt_cache(R"( WITH file AS ( SELECT file_id - FROM dds_source_files + FROM bpt_source_files WHERE path = ? ) SELECT path, input_mtime - FROM dds_compile_deps - JOIN dds_source_files ON input_file_id = file_id + FROM bpt_compile_deps + JOIN bpt_source_files ON input_file_id = file_id WHERE output_file_id IN file )"_sql); st.reset(); @@ -212,19 +221,19 @@ std::optional database::command_of(path_ref file_) const auto& st = _stmt_cache(R"( WITH file AS ( SELECT file_id - FROM dds_source_files + FROM bpt_source_files WHERE path = ? ) - SELECT command, output, avg_duration - FROM dds_compilations + SELECT command, output, avg_duration, toolchain_hash + FROM bpt_compilations WHERE file_id IN file )"_sql); st.reset(); st.bindings()[1] = file.generic_string(); - auto opt_res = nsql::unpack_single_opt(st); - if (!opt_res) { + auto opt_res = nsql::next(st); + if (opt_res.errc() == nsql::errc::done) { return std::nullopt; } - auto& [cmd, out, dur] = *opt_res; - return completed_compilation{cmd, out, std::chrono::milliseconds(dur)}; + auto& [cmd, out, dur, tc_id] = *opt_res; + return completed_compilation{cmd, out, tc_id, std::chrono::milliseconds(dur)}; } diff --git a/src/dds/db/database.hpp b/src/bpt/db/database.hpp similarity index 77% rename from src/dds/db/database.hpp rename to src/bpt/db/database.hpp index 280d4ccb..79254fe8 100644 --- a/src/dds/db/database.hpp +++ b/src/bpt/db/database.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include @@ -8,30 +8,34 @@ #include #include +#include #include #include #include #include -namespace dds { +namespace bpt { struct completed_compilation { - std::string quoted_command; - std::string output; + std::string quoted_command; + std::string output; + std::int64_t toolchain_hash; // The amount of time that the command took to run std::chrono::milliseconds duration; }; struct input_file_info { fs::path path; - fs::file_time_type last_mtime; + fs::file_time_type prev_mtime; }; class database { - neo::sqlite3::database _db; + neo::sqlite3::connection _db; mutable neo::sqlite3::statement_cache _stmt_cache{_db}; - explicit database(neo::sqlite3::database db); + std::map _stored_file_ids_cache; + + explicit database(neo::sqlite3::connection db); database(const database&) = delete; std::int64_t _record_file(path_ref p); @@ -52,4 +56,4 @@ class database { std::optional command_of(path_ref file) const; }; -} // namespace dds \ No newline at end of file +} // namespace bpt \ No newline at end of file diff --git a/src/bpt/db/database.test.cpp b/src/bpt/db/database.test.cpp new file mode 100644 index 00000000..06f99f7f --- /dev/null +++ b/src/bpt/db/database.test.cpp @@ -0,0 +1,7 @@ +#include + +#include + +using namespace std::literals; + +TEST_CASE("Create a database") { auto db = bpt::database::open(":memory:"s); } diff --git a/src/bpt/deps.cpp b/src/bpt/deps.cpp new file mode 100644 index 00000000..2c39294e --- /dev/null +++ b/src/bpt/deps.cpp @@ -0,0 +1,39 @@ +#include "./deps.hpp" + +#include +#include +#include +#include +#include +#include + +using namespace bpt; + +dependency_manifest dependency_manifest::from_file(path_ref fpath) { + BPT_E_SCOPE(e_parse_dependency_manifest_path{fpath}); + auto data = bpt::yaml_as_json5_data(bpt::parse_yaml_file(fpath)); + + dependency_manifest ret; + using namespace bpt::walk_utils; + + key_dym_tracker dym{{"dependencies"}}; + + // Parse and validate + walk( // + data, + require_mapping{"The root of a dependency manifest must be a JSON object"}, + mapping{ + dym.tracker(), + if_key{"$schema", just_accept}, + required_key{ + "dependencies", + "A 'dependencies' key is required", + require_array{"'dependencies' must be an array of strings"}, + for_each{put_into{std::back_inserter(ret.dependencies), + project_dependency::from_json_data}}, + }, + dym.rejecter(), + }); + + return ret; +} diff --git a/src/bpt/deps.hpp b/src/bpt/deps.hpp new file mode 100644 index 00000000..25ea12d2 --- /dev/null +++ b/src/bpt/deps.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include + +namespace bpt { + +struct e_parse_dependency_manifest_path { + fs::path value; +}; + +struct e_bad_deps_json_key : e_nonesuch { + using e_nonesuch::e_nonesuch; +}; + +/** + * Represents a dependency listing file, which is a subset of a package manifest + */ +struct dependency_manifest { + std::vector dependencies; + + static dependency_manifest from_file(path_ref where); +}; + +} // namespace bpt diff --git a/src/dds/dym.cpp b/src/bpt/dym.cpp similarity index 86% rename from src/dds/dym.cpp rename to src/bpt/dym.cpp index e1c25c4f..cc6b4f0b 100644 --- a/src/dds/dym.cpp +++ b/src/bpt/dym.cpp @@ -1,7 +1,7 @@ -#include +#include -#include -#include +#include +#include #include #include @@ -9,9 +9,9 @@ #include -using namespace dds; +using namespace bpt; -std::size_t dds::lev_edit_distance(std::string_view a, std::string_view b) noexcept { +std::size_t bpt::lev_edit_distance(std::string_view a, std::string_view b) noexcept { const auto n_rows = b.size() + 1; const auto n_columns = a.size() + 1; diff --git a/src/dds/dym.hpp b/src/bpt/dym.hpp similarity index 95% rename from src/dds/dym.hpp rename to src/bpt/dym.hpp index e285da94..826dfee7 100644 --- a/src/dds/dym.hpp +++ b/src/bpt/dym.hpp @@ -7,7 +7,7 @@ #include #include -namespace dds { +namespace bpt { std::size_t lev_edit_distance(std::string_view a, std::string_view b) noexcept; @@ -28,4 +28,4 @@ did_you_mean(std::string_view given, std::initializer_list str return did_you_mean(given, ranges::views::all(strings)); } -} // namespace dds \ No newline at end of file +} // namespace bpt \ No newline at end of file diff --git a/src/bpt/dym.test.cpp b/src/bpt/dym.test.cpp new file mode 100644 index 00000000..67db6ac7 --- /dev/null +++ b/src/bpt/dym.test.cpp @@ -0,0 +1,16 @@ +#include "./dym.hpp" + +#include + +TEST_CASE("Basic string distance") { + CHECK(bpt::lev_edit_distance("a", "a") == 0); + CHECK(bpt::lev_edit_distance("a", "b") == 1); + CHECK(bpt::lev_edit_distance("aa", "a") == 1); +} + +TEST_CASE("Find the 'did-you-mean' candidate") { + auto cand = bpt::did_you_mean("food", {"foo", "bar"}); + CHECK(cand == "foo"); + cand = bpt::did_you_mean("eatable", {"edible", "tangible"}); + CHECK(cand == "edible"); +} diff --git a/src/bpt/error/doc_ref.hpp b/src/bpt/error/doc_ref.hpp new file mode 100644 index 00000000..297cb3f6 --- /dev/null +++ b/src/bpt/error/doc_ref.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +namespace bpt { + +/** + * @brief A reference to a documentation page. + * + * Do not construct one directly. Use BPT_DOC_REF and BPT_ERROR_REF, which can be audited + * automatically for spelling errors and staleness. + */ +struct e_doc_ref { + std::string value; +}; + +} // namespace bpt + +#define BPT_DOC_REF(Page) (::bpt::e_doc_ref{::std::string(Page ".html")}) +#define BPT_ERR_REF(Page) (::bpt::e_doc_ref{::std::string("err/" Page ".html")}) diff --git a/src/bpt/error/errors.cpp b/src/bpt/error/errors.cpp new file mode 100644 index 00000000..3a1c236c --- /dev/null +++ b/src/bpt/error/errors.cpp @@ -0,0 +1,144 @@ +#include "./errors.hpp" + +#include +#include + +using namespace bpt; + +namespace { + +std::string error_url_prefix = "https://vector-of-bool.github.io/docs/bpt/err/"; + +std::string error_url_suffix(bpt::errc ec) noexcept { + switch (ec) { + case errc::invalid_builtin_toolchain: + return "invalid-builtin-toolchain.html"; + case errc::no_default_toolchain: + return "no-default-toolchain.html"; + case errc::test_failure: + return "test-failure.html"; + case errc::compile_failure: + return "compile-failure.html"; + case errc::archive_failure: + return "archive-failure.html"; + case errc::link_failure: + return "link-failure.html"; + case errc::invalid_remote_url: + return "invalid-remote-url.html"; + case errc::invalid_pkg_filesystem: + return "invalid-pkg-filesystem.html"; + case errc::sdist_exists: + return "sdist-exists.html"; + case errc::cyclic_usage: + return "cyclic-usage.html"; + case errc::none: + break; + } + assert(false && "Unreachable code path generating error explanation URL"); + std::terminate(); +} + +} // namespace + +std::string bpt::error_reference_of(bpt::errc ec) noexcept { + return error_url_prefix + error_url_suffix(ec); +} + +std::string_view bpt::explanation_of(bpt::errc ec) noexcept { + switch (ec) { + case errc::invalid_builtin_toolchain: + return R"( +If you start your toolchain name (The `-t` or `--toolchain` argument) +with a leading colon, bpt will interpret it as a reference to a built-in +toolchain. (Toolchain file paths cannot begin with a leading colon). + +These toolchain names are encoded into the bpt executable and cannot be +modified. +)"; + case errc::no_default_toolchain: + return R"( +`bpt` requires a toolchain to be specified in order to execute the build. `bpt` +will not perform a "best-guess" at a default toolchain. You may either pass the +name of a built-in toolchain, or write a "default toolchain" file to one of the +supported filepaths. Refer to the documentation for more information. +)"; + case errc::test_failure: + return R"( +One or more of the project's tests failed. The failing tests are listed above, +along with their exit code and output. +)"; + case errc::compile_failure: + return R"( +Source compilation failed. Refer to the compiler output. +)"; + case errc::archive_failure: + return R"( +Creating a static library archive failed, which prevents the associated library +from being used as this archive is the input to the linker for downstream +build targets. + +It is unlikely that regular user action can cause static library archiving to +fail. Refer to the output of the archiving tool. +)"; + case errc::link_failure: + return R"( +Linking a runtime binary file failed. There are a variety of possible causes +for this error. Refer to the documentation for more information. +)"; + case errc::invalid_remote_url: + return R"(The given package/remote URL is invalid)"; + case errc::invalid_pkg_filesystem: + return R"( +`bpt` prescribes a specific filesystem structure that must be obeyed by +libraries and packages. Refer to the documentation for an explanation and +reference on these prescriptions. +)"; + case errc::sdist_exists: + return R"( +By default, `bpt` will not overwrite source distributions that already exist +(either in the repository or a filesystem path). Such an action could +potentially destroy important data. +)"; + case errc::cyclic_usage: + return R"( +A cyclic dependency was detected among the libraries' `uses` fields. The cycle +must be removed. If no cycle is apparent, check that the `uses` field for the +library does not refer to the library itself. +)"; + case errc::none: + break; + } + assert(false && "Unexpected execution path during error explanation. This is a BPT bug"); + std::terminate(); +} + +#define BUG_STRING_SUFFIX " <- (Seeing this text is a `bpt` bug. Please report it.)" + +std::string_view bpt::default_error_string(bpt::errc ec) noexcept { + switch (ec) { + case errc::invalid_builtin_toolchain: + return "The built-in toolchain name is invalid"; + case errc::no_default_toolchain: + return "Unable to find a default toolchain to use for the build"; + case errc::test_failure: + return "One or more tests failed"; + case errc::compile_failure: + return "Source compilation failed."; + case errc::archive_failure: + return "Creating a static library archive failed"; + case errc::link_failure: + return "Linking a runtime binary (executable/shared library/DLL) failed"; + case errc::invalid_remote_url: + return "The given package/remote URL is not valid"; + case errc::invalid_pkg_filesystem: + return "The filesystem structure of the package/library is invalid." BUG_STRING_SUFFIX; + case errc::sdist_exists: + return "The source ditsribution already exists at the destination " BUG_STRING_SUFFIX; + case errc::cyclic_usage: + return "A cyclic dependency was detected among the libraries' `uses` fields."; + case errc::none: + break; + } + assert(false && "Unexpected execution path during error message creation. This is a BPT bug"); + std::terminate(); +} diff --git a/src/dds/error/errors.hpp b/src/bpt/error/errors.hpp similarity index 79% rename from src/dds/error/errors.hpp rename to src/bpt/error/errors.hpp index d856c0bb..45a6cd75 100644 --- a/src/dds/error/errors.hpp +++ b/src/bpt/error/errors.hpp @@ -5,48 +5,23 @@ #include #include -namespace dds { +namespace bpt { enum class errc { none = 0, invalid_builtin_toolchain, no_default_toolchain, - no_such_catalog_package, - git_url_ref_mutual_req, test_failure, compile_failure, archive_failure, link_failure, - catalog_too_new, - corrupted_catalog_db, - invalid_catalog_json, - no_catalog_remote_info, - - git_clone_failure, invalid_remote_url, - http_download_failure, - invalid_repo_transform, - sdist_ident_mismatch, sdist_exists, - corrupted_build_db, - - invalid_lib_manifest, - invalid_pkg_manifest, - invalid_version_range_string, - invalid_version_string, - invalid_pkg_id, - invalid_pkg_name, - unknown_test_driver, - dependency_resolve_failure, - dup_lib_name, - unknown_usage_name, + cyclic_usage, - invalid_lib_filesystem, invalid_pkg_filesystem, - - template_error, }; std::string error_reference_of(errc) noexcept; @@ -118,7 +93,7 @@ auto make_external_error() { template [[noreturn]] void throw_external_error(std::string_view fmt_str, Args&&... args) { - throw make_external_error(fmt::format(fmt_str, std::forward(args)...)); + throw make_external_error(fmt_str, std::forward(args)...); } template @@ -126,4 +101,4 @@ template throw make_external_error(std::string(default_error_string(ErrorCode))); } -} // namespace dds +} // namespace bpt diff --git a/src/bpt/error/exit.cpp b/src/bpt/error/exit.cpp new file mode 100644 index 00000000..b99e7aef --- /dev/null +++ b/src/bpt/error/exit.cpp @@ -0,0 +1,8 @@ +#include "./exit.hpp" + +#include +#include + +void bpt::throw_system_exit(int rc) { + BOOST_LEAF_THROW_EXCEPTION(boost::leaf::current_error(), e_exit{rc}); +} diff --git a/src/bpt/error/exit.hpp b/src/bpt/error/exit.hpp new file mode 100644 index 00000000..3e14cbf3 --- /dev/null +++ b/src/bpt/error/exit.hpp @@ -0,0 +1,18 @@ +#pragma once + +namespace bpt { + +/** + * @brief Error object telling the top-level error handler to exit normally with the given exit code + * + */ +struct e_exit { + int value; +}; + +/** + * @brief Throw an exceptoin with an e_exit for the given exit code. + */ +[[noreturn]] void throw_system_exit(int value); + +} // namespace bpt \ No newline at end of file diff --git a/src/bpt/error/handle.cpp b/src/bpt/error/handle.cpp new file mode 100644 index 00000000..342d58b7 --- /dev/null +++ b/src/bpt/error/handle.cpp @@ -0,0 +1,14 @@ +#include "./handle.hpp" + +#include + +#include +#include + +using namespace bpt; + +void bpt::leaf_handle_unknown_void(std::string_view message, + const boost::leaf::verbose_diagnostic_info& info) { + bpt_log(warn, message); + bpt_log(warn, "An unhandled error occurred:\n{}", info); +} diff --git a/src/bpt/error/handle.hpp b/src/bpt/error/handle.hpp new file mode 100644 index 00000000..5427a8c6 --- /dev/null +++ b/src/bpt/error/handle.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include + +#include + +namespace boost::leaf { + +class verbose_diagnostic_info; + +} // namespace boost::leaf + +namespace bpt { + +void leaf_handle_unknown_void(std::string_view message, + const boost::leaf::verbose_diagnostic_info&); + +template +auto leaf_handle_unknown(std::string_view message, T&& val) { + return [val = NEO_FWD(val), message](const boost::leaf::verbose_diagnostic_info& info) { + leaf_handle_unknown_void(message, info); + return val; + }; +} + +template +using matchv = boost::leaf::match; + +} // namespace bpt diff --git a/src/bpt/error/human.hpp b/src/bpt/error/human.hpp new file mode 100644 index 00000000..382b2357 --- /dev/null +++ b/src/bpt/error/human.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace bpt { + +struct e_human_message { + std::string value; +}; + +} // namespace bpt \ No newline at end of file diff --git a/src/bpt/error/marker.cpp b/src/bpt/error/marker.cpp new file mode 100644 index 00000000..394710a5 --- /dev/null +++ b/src/bpt/error/marker.cpp @@ -0,0 +1,14 @@ +#include "./marker.hpp" + +#include +#include +#include + +void bpt::write_error_marker(std::string_view error) noexcept { + bpt_log(trace, "[error marker {}]", error); + auto efile_path = bpt::getenv("BPT_WRITE_ERROR_MARKER"); + if (efile_path) { + bpt_log(trace, "[error marker written to [{}]]", *efile_path); + bpt::write_file(*efile_path, error); + } +} diff --git a/src/bpt/error/marker.hpp b/src/bpt/error/marker.hpp new file mode 100644 index 00000000..c54f5213 --- /dev/null +++ b/src/bpt/error/marker.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include +#include + +namespace bpt { + +void write_error_marker(std::string_view) noexcept; + +struct e_error_marker { + std::string value; +}; + +} // namespace bpt \ No newline at end of file diff --git a/src/bpt/error/nonesuch.cpp b/src/bpt/error/nonesuch.cpp new file mode 100644 index 00000000..4f221b29 --- /dev/null +++ b/src/bpt/error/nonesuch.cpp @@ -0,0 +1,24 @@ +#include "./nonesuch.hpp" + +#include + +#include + +#include + +using namespace bpt; +using namespace fansi::literals; + +void e_nonesuch::log_error(std::string_view fmt) const noexcept { + bpt_log(error, fmt, given); + if (nearest) { + bpt_log(error, " (Did you mean '.br.yellow[{}]'?)"_styled, *nearest); + } +} + +void bpt::e_nonesuch::ostream_into(std::ostream& out) const noexcept { + out << "bpt::e_nonsuch: Given " << std::quoted(given); + if (nearest.has_value()) { + out << " (nearest is " << std::quoted(*nearest) << ")"; + } +} \ No newline at end of file diff --git a/src/dds/error/nonesuch.hpp b/src/bpt/error/nonesuch.hpp similarity index 56% rename from src/dds/error/nonesuch.hpp rename to src/bpt/error/nonesuch.hpp index b7946d3d..968b0651 100644 --- a/src/dds/error/nonesuch.hpp +++ b/src/bpt/error/nonesuch.hpp @@ -3,7 +3,9 @@ #include #include -namespace dds { +#include + +namespace bpt { struct e_nonesuch { std::string given; @@ -14,6 +16,13 @@ struct e_nonesuch { , nearest{nr} {} void log_error(std::string_view fmt) const noexcept; + + void ostream_into(std::ostream& out) const noexcept; + + friend std::ostream& operator<<(std::ostream& out, const e_nonesuch& self) noexcept { + self.ostream_into(out); + return out; + } }; -} // namespace dds +} // namespace bpt diff --git a/src/dds/error/on_error.hpp b/src/bpt/error/on_error.hpp similarity index 74% rename from src/dds/error/on_error.hpp rename to src/bpt/error/on_error.hpp index c4b48fcf..3c50a9f7 100644 --- a/src/dds/error/on_error.hpp +++ b/src/bpt/error/on_error.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include /** @@ -7,11 +9,11 @@ * * Use this as a parameter to leaf's error-loading APIs. */ -#define DDS_E_ARG(...) ([&] { return __VA_ARGS__; }) +#define BPT_E_ARG(...) ([&] { return __VA_ARGS__; }) /** * @brief Generate a leaf::on_error object that loads the given expression into the currently * in-flight error if the current scope is exitted via exception or a bad result<> */ -#define DDS_E_SCOPE(...) \ - auto NEO_CONCAT(_err_info_, __LINE__) = boost::leaf::on_error(DDS_E_ARG(__VA_ARGS__)) +#define BPT_E_SCOPE(...) \ + auto NEO_CONCAT(_err_info_, __LINE__) = boost::leaf::on_error(BPT_E_ARG(__VA_ARGS__)) diff --git a/src/dds/error/result.hpp b/src/bpt/error/result.hpp similarity index 60% rename from src/dds/error/result.hpp rename to src/bpt/error/result.hpp index 36c28dac..62fae471 100644 --- a/src/dds/error/result.hpp +++ b/src/bpt/error/result.hpp @@ -1,12 +1,14 @@ #pragma once +#include "./human.hpp" #include "./result_fwd.hpp" #include #include -namespace dds { +namespace bpt { +using boost::leaf::current_error; using boost::leaf::new_error; -} // namespace dds +} // namespace bpt diff --git a/src/dds/error/result_fwd.hpp b/src/bpt/error/result_fwd.hpp similarity index 60% rename from src/dds/error/result_fwd.hpp rename to src/bpt/error/result_fwd.hpp index f7efb901..38874fe6 100644 --- a/src/dds/error/result_fwd.hpp +++ b/src/bpt/error/result_fwd.hpp @@ -2,13 +2,16 @@ namespace boost::leaf { +class bad_result; + template class result; } // namespace boost::leaf -namespace dds { +namespace bpt { +using boost::leaf::bad_result; using boost::leaf::result; -} // namespace dds +} // namespace bpt diff --git a/src/bpt/error/toolchain.hpp b/src/bpt/error/toolchain.hpp new file mode 100644 index 00000000..93ee0aed --- /dev/null +++ b/src/bpt/error/toolchain.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace bpt { + +struct e_loading_toolchain { + std::string value; +}; + +} // namespace bpt \ No newline at end of file diff --git a/src/bpt/error/try_catch.hpp b/src/bpt/error/try_catch.hpp new file mode 100644 index 00000000..99476f6d --- /dev/null +++ b/src/bpt/error/try_catch.hpp @@ -0,0 +1,112 @@ +#pragma once + +#include +#include + +#include +#include + +namespace bpt { + +template +struct leaf_handler_seq { + using result_type = std::invoke_result_t; + Try& try_block; + + std::tuple handlers{}; + + template + constexpr auto operator*(Catch c) const noexcept { + return leaf_handler_seq{ + try_block, std::tuple_cat(handlers, std::tie(c.h))}; + } + + constexpr decltype(auto) invoke(auto handle_all) const { + constexpr auto has_handlers = sizeof...(Handlers) != 0; + static_assert(has_handlers, "bpt_leaf_try requires one or more bpt_leaf_catch blocks"); + return std::apply( + [&](auto&... hs) { + if constexpr (handle_all.value) { + if constexpr (boost::leaf::is_result_type::value) { + return boost::leaf::try_handle_all(try_block, hs...); + } else { + return boost::leaf::try_catch(try_block, hs...); + } + } else { + if constexpr (boost::leaf::is_result_type::value) { + return boost::leaf::try_handle_some(try_block, hs...); + } else { + return boost::leaf::try_handle_some( + [&]() -> boost::leaf::result { return try_block(); }, + hs...); + } + } + }, + handlers); + } +}; + +struct leaf_make_try_block { + template + constexpr decltype(auto) operator->*(Func&& block) const { + return leaf_handler_seq{block}; + } +}; + +template +struct leaf_catch_block { + using handler_type = H; + + handler_type& h; + + leaf_catch_block(H& h) + : h(h) {} +}; + +struct leaf_make_catch_block { + template + constexpr decltype(auto) operator->*(Func&& block) const { + return leaf_catch_block>{block}; + } +}; + +template +struct leaf_exec_try_catch_sequence { + template + constexpr decltype(auto) operator+(const leaf_handler_seq seq) const { + return seq.invoke(std::bool_constant{}); + } +}; + +using boost::leaf::catch_; + +struct noreturn_t { + template + constexpr operator T() const noexcept { + std::terminate(); + } +}; + +} // namespace bpt + +/** + * @brief Create a try {} block that handles all errors using Boost.LEAF + */ +#define bpt_leaf_try \ + ::bpt::leaf_exec_try_catch_sequence{} + ::bpt::leaf_make_try_block{}->*[&]() + +/** + * @brief Create a try {} block that handles errors using Boost.LEAF + * + * The result is always a result type. + */ +#define bpt_leaf_try_some \ + ::bpt::leaf_exec_try_catch_sequence{} + ::bpt::leaf_make_try_block{}->*[&]() + +/** + * @brief Create an error handling block for Boost.LEAF + */ +#define bpt_leaf_catch *::bpt::leaf_make_catch_block{}->*[&] + +#define bpt_leaf_catch_all \ + bpt_leaf_catch(::boost::leaf::verbose_diagnostic_info const& diagnostic_info [[maybe_unused]]) diff --git a/src/bpt/error/try_catch.test.cpp b/src/bpt/error/try_catch.test.cpp new file mode 100644 index 00000000..444f9d9a --- /dev/null +++ b/src/bpt/error/try_catch.test.cpp @@ -0,0 +1,9 @@ +#include "./try_catch.hpp" + +#include + +TEST_CASE("Try-catch") { + auto r = bpt_leaf_try { return 2; } + bpt_leaf_catch_all->int { return 0; }; + CHECK(r == 2); +} \ No newline at end of file diff --git a/src/bpt/project/dependency.cpp b/src/bpt/project/dependency.cpp new file mode 100644 index 00000000..a23d9140 --- /dev/null +++ b/src/bpt/project/dependency.cpp @@ -0,0 +1,112 @@ +#include "./dependency.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace bpt; + +crs::dependency project_dependency::as_crs_dependency() const noexcept { + return crs::dependency{ + .name = dep_name, + .acceptable_versions = acceptable_versions, + .uses = using_, + }; +} + +project_dependency project_dependency::parse_dep_range_shorthand(std::string_view const sv) { + BPT_E_SCOPE(e_parse_dep_range_shorthand_string{std::string(sv)}); + + project_dependency ret; + + auto sep_pos = sv.find_first_of("=@^~+"); + if (sep_pos == sv.npos) { + BOOST_LEAF_THROW_EXCEPTION( + e_human_message{"Expected one of '=@^~+' in name+version shorthand"}); + } + + ret.dep_name = bpt::name::from_string(sv.substr(0, sep_pos)).value(); + + auto range_str = std::string(sv.substr(sep_pos)); + if (range_str.front() == '@') { + range_str[0] = '^'; + } + const semver::range rng = semver::range::parse_restricted(range_str); + + ret.acceptable_versions = {rng.low(), rng.high()}; + return ret; +} + +static std::string_view next_token(std::string_view sv) noexcept { + sv = trim_view(sv); + if (sv.empty()) { + return sv; + } + auto it = sv.begin(); + if (*it == ',') { + return sv.substr(0, 1); + } + while (it != sv.end() && !std::isspace(*it) && *it != ',') { + ++it; + } + auto len = it - sv.begin(); + return sv.substr(0, len); +} + +project_dependency project_dependency::from_shorthand_string(const std::string_view sv) { + BPT_E_SCOPE(e_parse_dep_shorthand_string{std::string(sv)}); + std::string_view remain = sv; + std::string_view tok = remain.substr(0, 0); + auto adv_token = [&] { + remain.remove_prefix(tok.data() - remain.data()); + remain.remove_prefix(tok.size()); + return tok = next_token(remain); + }; + + adv_token(); + if (tok.empty()) { + BOOST_LEAF_THROW_EXCEPTION(e_human_message{"Invalid empty dependency specifier"}); + } + + auto ret = parse_dep_range_shorthand(tok); + + adv_token(); + if (tok.empty()) { + ret.using_.push_back(ret.dep_name); + return ret; + } + + if (tok != "using") { + BOOST_LEAF_THROW_EXCEPTION(e_human_message{ + neo::ufmt("Expected 'using' following dependency name and range (Got '{}')", tok)}); + } + + if (tok == "using") { + while (1) { + adv_token(); + if (tok == ",") { + BOOST_LEAF_THROW_EXCEPTION( + e_human_message{"Unexpected extra comma in dependency specifier"}); + } + ret.using_.emplace_back(*bpt::name::from_string(tok)); + if (adv_token() != ",") { + break; + } + } + } + + if (!tok.empty()) { + BOOST_LEAF_THROW_EXCEPTION(e_human_message{ + neo::ufmt("Unexpected trailing string in dependency string \"{}\"", remain)}); + } + + return ret; +} diff --git a/src/bpt/project/dependency.hpp b/src/bpt/project/dependency.hpp new file mode 100644 index 00000000..00b1dc6f --- /dev/null +++ b/src/bpt/project/dependency.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +#include + +#include +#include + +namespace bpt { + +struct e_parse_dep_shorthand_string { + std::string value; +}; + +struct e_parse_dep_range_shorthand_string { + std::string value; +}; + +/** + * @brief A depedency declared by a project (Not a CRS dist) + */ +struct project_dependency { + /// The name of the dependency package + bpt::name dep_name; + /// A set of version ranges that are acceptable + crs::version_range_set acceptable_versions; + /// Libraries from the dependency that will be explicitly used + std::vector using_{}; + + static project_dependency parse_dep_range_shorthand(std::string_view sv); + static project_dependency from_shorthand_string(std::string_view sv); + static project_dependency from_json_data(const json5::data&); + + crs::dependency as_crs_dependency() const noexcept; +}; + +} // namespace bpt diff --git a/src/bpt/project/dependency.test.cpp b/src/bpt/project/dependency.test.cpp new file mode 100644 index 00000000..8cc93e72 --- /dev/null +++ b/src/bpt/project/dependency.test.cpp @@ -0,0 +1,44 @@ +#include "./dependency.hpp" + +#include + +#include + +bpt::crs::version_range_set simple_ver_range(std::string_view from, std::string_view until) { + return bpt::crs::version_range_set(semver::version::parse(from), semver::version::parse(until)); +} + +#ifndef _MSC_VER // MSVC struggles with compiling this test case +TEST_CASE("Parse a shorthand") { + struct case_ { + std::string_view given; + bpt::project_dependency expect; + }; + auto [given, expect] = GENERATE(Catch::Generators::values({ + { + .given = "foo@1.2.3", + .expect = {{"foo"}, simple_ver_range("1.2.3", "2.0.0"), {bpt::name{"foo"}}}, + }, + { + .given = "foo~1.2.3 ", + .expect = {{"foo"}, simple_ver_range("1.2.3", "1.3.0"), {bpt::name{"foo"}}}, + }, + { + .given = " foo=1.2.3", + .expect = {{"foo"}, simple_ver_range("1.2.3", "1.2.4"), {bpt::name{"foo"}}}, + }, + { + .given = "foo@1.2.3 using bar , baz", + .expect = {"foo", + simple_ver_range("1.2.3", "2.0.0"), + std::vector({bpt::name{"bar"}, bpt::name{"baz"}})}, + }, + })); + + auto dep = REQUIRES_LEAF_NOFAIL(bpt::project_dependency::from_shorthand_string(given)); + + CHECK(dep.dep_name == expect.dep_name); + CHECK(dep.using_ == expect.using_); + CHECK(dep.acceptable_versions == expect.acceptable_versions); +} +#endif diff --git a/src/bpt/project/dependency.walk.cpp b/src/bpt/project/dependency.walk.cpp new file mode 100644 index 00000000..ea841042 --- /dev/null +++ b/src/bpt/project/dependency.walk.cpp @@ -0,0 +1,101 @@ +#include "./dependency.hpp" + +#include "./error.hpp" + +#include +#include +#include + +#include +#include + +using namespace bpt; +using namespace bpt::walk_utils; + +static auto parse_version_range = [](const json5::data& range) { + semver::version low; + semver::version high; + key_dym_tracker dym; + walk(range, + require_mapping{"'versions' elements must be objects"}, + mapping{ + dym.tracker(), + required_key{"low", + "'low' version is required", + require_str{"'low' version must be a string"}, + put_into{low, version_from_string{}}}, + required_key{"high", + "'high' version is required", + require_str{"'high' version must be a string"}, + put_into{high, version_from_string{}}}, + if_key{"_comment", just_accept}, + dym.rejecter(), + }); + if (high <= low) { + throw(semester::walk_error{"'high' version must be strictly greater than 'low' version"}); + } + return semver::range{low, high}; +}; + +project_dependency project_dependency::from_json_data(const json5::data& data) { + if (data.is_string()) { + return project_dependency::from_shorthand_string(data.as_string()); + } + + project_dependency ret; + + bool got_versions_key = false; + std::vector explicit_uses; + bool got_using_key = false; + std::vector ver_ranges; + + key_dym_tracker dym{{"dep", "versions", "using"}}; + + walk( // + data, + require_mapping{"Dependency must be a shorthand string or a JSON object"}, + mapping{ + dym.tracker(), + required_key{ + "dep", + "A 'dep' key is required in a dependency object", + require_str{"Dependency 'dep' must be a string"}, + // We'll handle this later on, since handling depends on the presence/absence of + // 'versions' + just_accept, + }, + if_key{"versions", + require_array{"Dependency 'versions' must be an array of objects"}, + set_true{got_versions_key}, + for_each{require_mapping{"Each 'versions' element must be an object"}, + put_into(std::back_inserter(ver_ranges), parse_version_range)}}, + if_key{"using", + require_array{"Dependency 'using' must be an array"}, + for_each{require_str{"Each dependency 'using' item must be a string"}, + set_true{got_using_key}, + put_into(std::back_inserter(explicit_uses), name_from_string{})}}, + dym.rejecter(), + }); + + for (auto& ver : ver_ranges) { + ret.acceptable_versions = ret.acceptable_versions.union_( + pubgrub::interval_set{ver.low(), ver.high()}); + } + + auto& dep_string = data.as_object().find("dep")->second.as_string(); + if (got_versions_key) { + ret.dep_name = name_from_string{}(dep_string); + } else { + auto partial = parse_dep_range_shorthand(dep_string); + ret.dep_name = partial.dep_name; + ret.acceptable_versions = partial.acceptable_versions; + } + + if (got_using_key) { + ret.using_ = std::move(explicit_uses); + } else { + ret.using_.push_back(ret.dep_name); + } + + return ret; +} \ No newline at end of file diff --git a/src/bpt/project/error.hpp b/src/bpt/project/error.hpp new file mode 100644 index 00000000..cc2ce21d --- /dev/null +++ b/src/bpt/project/error.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +#include + +namespace bpt { + +struct e_parse_project_manifest_path { + std::filesystem::path value; +}; + +struct e_parse_project_manifest_data { + json5::data value; +}; + +struct e_open_project { + std::filesystem::path value; +}; + +struct e_bad_bpt_yaml_key : e_nonesuch { + using e_nonesuch::e_nonesuch; +}; + +} // namespace bpt \ No newline at end of file diff --git a/src/bpt/project/fwd.hpp b/src/bpt/project/fwd.hpp new file mode 100644 index 00000000..d8a3231b --- /dev/null +++ b/src/bpt/project/fwd.hpp @@ -0,0 +1,10 @@ +#pragma once + +namespace bpt { + +struct project; +struct project_manifest; +struct project_library; +struct project_dependency; + +} // namespace bpt \ No newline at end of file diff --git a/src/bpt/project/library.hpp b/src/bpt/project/library.hpp new file mode 100644 index 00000000..1f4b53e5 --- /dev/null +++ b/src/bpt/project/library.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "./dependency.hpp" +#include +#include + +#include + +#include +#include +#include + +namespace bpt { + +struct project_library { + /// The name of the library within the project + bpt::name name; + /// The relative path to the library root from the root of the project + std::filesystem::path relpath; + /// Libraries in the same project that are used by this library + std::vector intra_using; + std::vector intra_test_using; + /// Dependencies for this specific library + std::vector lib_dependencies; + std::vector test_dependencies; + + static project_library from_json_data(const json5::data&); +}; + +} // namespace bpt diff --git a/src/bpt/project/library.walk.cpp b/src/bpt/project/library.walk.cpp new file mode 100644 index 00000000..2a14f9c3 --- /dev/null +++ b/src/bpt/project/library.walk.cpp @@ -0,0 +1,56 @@ +#include "./library.hpp" + +#include "./error.hpp" + +#include + +using namespace bpt; +using namespace bpt::walk_utils; + +project_library project_library::from_json_data(const json5::data& data) { + project_library ret; + + auto record_usings = [](auto key, auto& into) { + return walk_seq{require_str{neo::ufmt("Each '{}' must be a string", key)}, + put_into{std::back_inserter(into), name_from_string{}}}; + }; + + key_dym_tracker dym{{"name", "path", "using", "test-using", "dependencies"}}; + + walk(data, + require_mapping{"Library entries must be a mapping (JSON object)"}, + mapping{ + dym.tracker(), + required_key{"name", + "Library must have a 'name' string", + require_str{"Library 'name' must be a string"}, + put_into(ret.name, name_from_string{})}, + required_key{"path", + // The main 'lib' library in a project must not have a 'path' + // property, but this condition is handled in the + // project_manifest::from_json_data function, which checks and inserts + // a single '.' for 'path' + "Library must have a 'path' string", + put_into(ret.relpath, + [](std::string s) { return std::filesystem::path{s}; })}, + if_key{ + "using", + require_array{"Library 'using' key must be an array of strings"}, + for_each{record_usings("using", ret.intra_using)}, + }, + if_key{"test-using", + require_array{"Library 'test-using' key must be an array of strings"}, + for_each{record_usings("test-using", ret.intra_test_using)}}, + if_key{"dependencies", + require_array{"Library 'dependencies' must be an array of dependencies"}, + for_each{put_into(std::back_inserter(ret.lib_dependencies), + project_dependency::from_json_data)}}, + if_key{"test-dependencies", + require_array{"Library 'test-dependencies' must be an array of dependencies"}, + for_each{put_into(std::back_inserter(ret.test_dependencies), + project_dependency::from_json_data)}}, + dym.rejecter(), + }); + + return ret; +} \ No newline at end of file diff --git a/src/bpt/project/project.cpp b/src/bpt/project/project.cpp new file mode 100644 index 00000000..34501834 --- /dev/null +++ b/src/bpt/project/project.cpp @@ -0,0 +1,115 @@ +#include "./project.hpp" + +#include "./error.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace bpt; + +project project::open_directory(path_ref path_) { + BPT_E_SCOPE(e_open_project{path_}); + auto path = resolve_path_strong(path_).value(); + auto bpt_yaml = path / "bpt.yaml"; + if (!bpt::file_exists(bpt_yaml)) { + if (bpt::file_exists(path / "bpt.yml")) { + bpt_log(warn, + "There's a [bpt.yml] file in the project directory, but bpt expects a '.yaml' " + "file extension. The file [{}] will be ignored.", + (path / "bpt.yml").string()); + } + return project{path, std::nullopt}; + } + BPT_E_SCOPE(e_parse_project_manifest_path{bpt_yaml}); + auto yml = bpt::parse_yaml_file(bpt_yaml); + auto data = bpt::yaml_as_json5_data(yml); + auto man = project_manifest::from_json_data(data, path); + return project{path, man}; +} + +crs::package_info project_manifest::as_crs_package_meta() const noexcept { + crs::package_info ret; + ret.id.name = name; + ret.id.version = version; + ret.id.revision = 1; + + auto libs = libraries // + | std::views::transform([&](project_library lib) { + auto deps_as_crs + = std::views::transform(&project_dependency::as_crs_dependency); + auto deps = lib.lib_dependencies | deps_as_crs | neo::to_vector; + extend(deps, root_dependencies | deps_as_crs); + auto test_deps = lib.test_dependencies | deps_as_crs | neo::to_vector; + extend(test_deps, root_test_dependencies | deps_as_crs); + return crs::library_info{ + .name = lib.name, + .path = lib.relpath, + .intra_using = lib.intra_using, + .intra_test_using = lib.intra_test_using, + .dependencies = std::move(deps), + .test_dependencies = std::move(test_deps), + }; + }); + ret.libraries = neo::to_vector(libs); + if (ret.libraries.empty()) { + ret.libraries.push_back(crs::library_info{ + .name = name, + .path = ".", + .intra_using = {}, + .intra_test_using = {}, + .dependencies = {}, + .test_dependencies = {}, + }); + extend(ret.libraries.back().dependencies, + root_dependencies | std::views::transform(BPT_TL(_1.as_crs_dependency()))); + extend(ret.libraries.back().test_dependencies, + root_test_dependencies | std::views::transform(BPT_TL(_1.as_crs_dependency()))); + } + + auto meta_obj = json5::data::object_type(); + if (authors) { + meta_obj.emplace("authors", + *authors // + | std::views::transform(BPT_TL(json5::data(_1))) // + | neo::to_vector); + } + if (description) { + meta_obj.emplace("description", *description); + } + if (documentation) { + meta_obj.emplace("documentation", documentation->normalized().to_string()); + } + if (readme) { + meta_obj.emplace("readme", readme->string()); + } + if (homepage) { + meta_obj.emplace("homepage", homepage->normalized().to_string()); + } + if (repository) { + meta_obj.emplace("repository", repository->normalized().to_string()); + } + if (license) { + meta_obj.emplace("license", license->to_string()); + } + if (license_file) { + meta_obj.emplace("license-file", license_file->string()); + } + if (!meta_obj.empty()) { + ret.meta = std::move(meta_obj); + } + return ret; +} diff --git a/src/bpt/project/project.hpp b/src/bpt/project/project.hpp new file mode 100644 index 00000000..e63836e5 --- /dev/null +++ b/src/bpt/project/project.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include "./library.hpp" + +#include "./spdx.hpp" + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +namespace bpt { + +struct project_manifest { + bpt::name name; + semver::version version; + + std::vector libraries; + std::vector root_dependencies; + std::vector root_test_dependencies; + + std::optional> authors; + std::optional description; + std::optional documentation; + std::optional readme; + std::optional homepage; + std::optional repository; + std::optional license; + std::optional license_file; + + bool is_private = false; + + static project_manifest from_json_data(const json5::data& data, + std::optional const& proj_dir); + + crs::package_info as_crs_package_meta() const noexcept; +}; + +struct project { + std::filesystem::path path; + std::optional manifest; + + static project open_directory(const std::filesystem::path&); +}; + +} // namespace bpt diff --git a/src/bpt/project/project.test.cpp b/src/bpt/project/project.test.cpp new file mode 100644 index 00000000..bc6f7787 --- /dev/null +++ b/src/bpt/project/project.test.cpp @@ -0,0 +1,10 @@ +#include "./project.hpp" + +#include + +#include + +TEST_CASE("Open a project directory") { + auto proj = REQUIRES_LEAF_NOFAIL( + bpt::project::open_directory(bpt::testing::DATA_DIR / "projects/simple")); +} diff --git a/src/bpt/project/project.walk.cpp b/src/bpt/project/project.walk.cpp new file mode 100644 index 00000000..61fc4d4a --- /dev/null +++ b/src/bpt/project/project.walk.cpp @@ -0,0 +1,164 @@ +#include "./project.hpp" + +#include "./error.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace bpt; +using namespace bpt::walk_utils; +using namespace fansi::literals; + +namespace { + +auto path_key(std::string_view key, + std::optional& into, + const std::optional& proj_dir) { + return if_key{ + std::string(key), + require_str{ + neo::ufmt("Project's '.bold.yellow[{}]' property must be a file path string"_styled, + key)}, + [key, &into, proj_dir](std::string const& s) { + if (proj_dir) { + std::filesystem::path abs = bpt::resolve_path_weak(*proj_dir / s); + if (!bpt::file_exists(abs)) { + bpt_log( + warn, + "Property '.blue[{}]' refers to non-existent path [.bold.yellow[{}]]"_styled, + key, + abs.string()); + } + } + into = normalize_path(s); + return walk.accept; + }, + }; +} + +auto url_key(std::string_view key, std::optional& into) { + return if_key{ + std::string(key), + require_str{ + neo::ufmt("Project's '.bold.yellow[{}]' property must be a URL string"_styled, key)}, + [key, &into](std::string const& s) { + into = bpt::parse_url(s); + return walk.accept; + }, + }; +} + +} // namespace + +project_manifest +project_manifest::from_json_data(const json5::data& data, + const std::optional& proj_dir) { + BPT_E_SCOPE(e_parse_project_manifest_data{data}); + project_manifest ret; + + key_dym_tracker dym{ + { + "name", + "version", + "dependencies", + "test-dependencies", + "libraries", + "readme", + "license-file", + "homepage", + "repository", + "documentation", + "description", + "license", + "authors", + }, + }; + + std::vector authors; + bool explicit_libraries = false; + bool has_authors = false; + + walk(data, + require_mapping{"Project manifest root must be a mapping (JSON object)"}, + mapping{ + dym.tracker(), + required_key{"name", + "A project 'name' is required", + require_str{"Project 'name' must be a string"}, + put_into{ret.name, name_from_string{}}}, + required_key{"version", + "A project 'version' is required", + require_str{"Project 'version' must be a string"}, + put_into{ret.version, version_from_string{}}}, + if_key{"dependencies", + require_array{"Project 'dependencies' should be an array"}, + for_each{put_into{std::back_inserter(ret.root_dependencies), + project_dependency::from_json_data}}}, + if_key{"test-dependencies", + require_array{"Project 'test-dependencies' should be an array"}, + for_each{put_into{std::back_inserter(ret.root_test_dependencies), + project_dependency::from_json_data}}}, + if_key{"libraries", + set_true{explicit_libraries}, + require_array{"Project 'libraries' should be an array"}, + for_each{put_into(std::back_inserter(ret.libraries), + project_library::from_json_data)}}, + if_key{"$schema", just_accept}, + if_key{"x", just_accept}, + // Other package metadata + path_key("readme", ret.readme, proj_dir), + path_key("license-file", ret.license_file, proj_dir), + url_key("homepage", ret.homepage), + url_key("repository", ret.repository), + url_key("documentation", ret.documentation), + if_key{"description", + require_str{"Project 'description' must be a string"}, + put_into(ret.description)}, + if_key{"license", + require_str("Project 'license' must be an SPDX license expression string"), + put_into(ret.license, + [](std::string s) { return bpt::spdx_license_expression::parse(s); })}, + if_key{"authors", + set_true{has_authors}, + require_array{"Project 'authors' must be an array of strings"}, + for_each{require_str{"Each element of project 'authors' must be a string"}, + put_into(std::back_inserter(authors))}}, + dym.rejecter(), + }); + + if (has_authors) { + ret.authors = std::move(authors); + } + + if (explicit_libraries) { + if (ret.libraries.empty()) { + throw walk_error{"Project's 'libraries' array may not be empty"}; + } + } else { + // No explicit 'libraries' key: Generate the default library for the + // project: + project_library def; + def.name = ret.name; + def.relpath = "."; + ret.libraries.push_back(std::move(def)); + } + + ret.as_crs_package_meta().throw_if_invalid(); + return ret; +} diff --git a/src/bpt/project/spdx-exc.inl b/src/bpt/project/spdx-exc.inl new file mode 100644 index 00000000..f8996f2c --- /dev/null +++ b/src/bpt/project/spdx-exc.inl @@ -0,0 +1,41 @@ +SPDX_EXCEPTION(R"_(389-exception)_", R"_(389 Directory Server Exception)_") +SPDX_EXCEPTION(R"_(Autoconf-exception-2.0)_", R"_(Autoconf exception 2.0)_") +SPDX_EXCEPTION(R"_(Autoconf-exception-3.0)_", R"_(Autoconf exception 3.0)_") +SPDX_EXCEPTION(R"_(Bison-exception-2.2)_", R"_(Bison exception 2.2)_") +SPDX_EXCEPTION(R"_(Bootloader-exception)_", R"_(Bootloader Distribution Exception)_") +SPDX_EXCEPTION(R"_(CLISP-exception-2.0)_", R"_(CLISP exception 2.0)_") +SPDX_EXCEPTION(R"_(Classpath-exception-2.0)_", R"_(Classpath exception 2.0)_") +SPDX_EXCEPTION(R"_(DigiRule-FOSS-exception)_", R"_(DigiRule FOSS License Exception)_") +SPDX_EXCEPTION(R"_(FLTK-exception)_", R"_(FLTK exception)_") +SPDX_EXCEPTION(R"_(Fawkes-Runtime-exception)_", R"_(Fawkes Runtime Exception)_") +SPDX_EXCEPTION(R"_(Font-exception-2.0)_", R"_(Font exception 2.0)_") +SPDX_EXCEPTION(R"_(GCC-exception-2.0)_", R"_(GCC Runtime Library exception 2.0)_") +SPDX_EXCEPTION(R"_(GCC-exception-3.1)_", R"_(GCC Runtime Library exception 3.1)_") +SPDX_EXCEPTION(R"_(GPL-3.0-linking-exception)_", R"_(GPL-3.0 Linking Exception)_") +SPDX_EXCEPTION(R"_(GPL-3.0-linking-source-exception)_", R"_(GPL-3.0 Linking Exception (with Corresponding Source))_") +SPDX_EXCEPTION(R"_(GPL-CC-1.0)_", R"_(GPL Cooperation Commitment 1.0)_") +SPDX_EXCEPTION(R"_(LGPL-3.0-linking-exception)_", R"_(LGPL-3.0 Linking Exception)_") +SPDX_EXCEPTION(R"_(LLVM-exception)_", R"_(LLVM Exception)_") +SPDX_EXCEPTION(R"_(LZMA-exception)_", R"_(LZMA exception)_") +SPDX_EXCEPTION(R"_(Libtool-exception)_", R"_(Libtool Exception)_") +SPDX_EXCEPTION(R"_(Linux-syscall-note)_", R"_(Linux Syscall Note)_") +SPDX_EXCEPTION(R"_(Nokia-Qt-exception-1.1)_", R"_(Nokia Qt LGPL exception 1.1)_") +SPDX_EXCEPTION(R"_(OCCT-exception-1.0)_", R"_(Open CASCADE Exception 1.0)_") +SPDX_EXCEPTION(R"_(OCaml-LGPL-linking-exception)_", R"_(OCaml LGPL Linking Exception)_") +SPDX_EXCEPTION(R"_(OpenJDK-assembly-exception-1.0)_", R"_(OpenJDK Assembly exception 1.0)_") +SPDX_EXCEPTION(R"_(PS-or-PDF-font-exception-20170817)_", R"_(PS/PDF font exception (2017-08-17))_") +SPDX_EXCEPTION(R"_(Qt-GPL-exception-1.0)_", R"_(Qt GPL exception 1.0)_") +SPDX_EXCEPTION(R"_(Qt-LGPL-exception-1.1)_", R"_(Qt LGPL exception 1.1)_") +SPDX_EXCEPTION(R"_(Qwt-exception-1.0)_", R"_(Qwt exception 1.0)_") +SPDX_EXCEPTION(R"_(SHL-2.0)_", R"_(Solderpad Hardware License v2.0)_") +SPDX_EXCEPTION(R"_(SHL-2.1)_", R"_(Solderpad Hardware License v2.1)_") +SPDX_EXCEPTION(R"_(Swift-exception)_", R"_(Swift Exception)_") +SPDX_EXCEPTION(R"_(Universal-FOSS-exception-1.0)_", R"_(Universal FOSS Exception, Version 1.0)_") +SPDX_EXCEPTION(R"_(WxWindows-exception-3.1)_", R"_(WxWindows Library Exception 3.1)_") +SPDX_EXCEPTION(R"_(eCos-exception-2.0)_", R"_(eCos exception 2.0)_") +SPDX_EXCEPTION(R"_(freertos-exception-2.0)_", R"_(FreeRTOS Exception 2.0)_") +SPDX_EXCEPTION(R"_(gnu-javamail-exception)_", R"_(GNU JavaMail exception)_") +SPDX_EXCEPTION(R"_(i2p-gpl-java-exception)_", R"_(i2p GPL+Java Exception)_") +SPDX_EXCEPTION(R"_(mif-exception)_", R"_(Macros and Inline Functions Exception)_") +SPDX_EXCEPTION(R"_(openvpn-openssl-exception)_", R"_(OpenVPN OpenSSL Exception)_") +SPDX_EXCEPTION(R"_(u-boot-exception-2.0)_", R"_(U-Boot exception 2.0)_") \ No newline at end of file diff --git a/src/bpt/project/spdx.cpp b/src/bpt/project/spdx.cpp new file mode 100644 index 00000000..1c00d162 --- /dev/null +++ b/src/bpt/project/spdx.cpp @@ -0,0 +1,214 @@ +#include "./spdx.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +using namespace bpt; + +namespace { + +const bpt::spdx_license_info ALL_LICENSES[] = { +#define SPDX_LICENSE(ID, Name) bpt::spdx_license_info{.id = ID, .name = Name}, +#include "./spdx.inl" +}; + +const bpt::spdx_exception_info ALL_EXCEPTIONS[] = { +#define SPDX_EXCEPTION(ID, Name) bpt::spdx_exception_info{.id = ID, .name = Name}, +#include "./spdx-exc.inl" +}; + +} // namespace + +const std::span bpt::spdx_license_info::all = ALL_LICENSES; +const std::span bpt::spdx_exception_info::all = ALL_EXCEPTIONS; + +namespace { + +constexpr bool is_idstring_char(char32_t c) noexcept { + if (neo::between(c, 'a', 'z') || neo::between(c, 'A', 'Z') || neo::between(c, '0', '9')) { + return true; + } + if (c == '-' || c == '.') { + return true; + } + return false; +} + +std::string_view next_token(std::string_view sv) { + sv = bpt::trim_view(sv); + auto it = sv.begin(); + if (it == sv.end()) { + return sv; + } + if (*it == neo::oper::any_of('(', ')', '+')) { + return sv.substr(0, 1); + } + while (it != sv.end() && is_idstring_char(*it)) { + ++it; + } + return bpt::sview(sv.begin(), it); +} + +template +neo::opt_ref find_with_id(std::string_view id) { + auto found = std::ranges::lower_bound(T::all, id, std::less<>{}, BPT_TL(_1.id)); + if (found == std::ranges::end(T::all) || found->id != id) { + return std::nullopt; + } + return *found; +} + +struct compound_expression; + +struct just_license_id { + spdx_license_info license; +}; +struct license_id_plus { + spdx_license_info license; +}; +struct license_ref {}; + +struct simple_expression + : bpt::variant_wrapper { + using variant_wrapper::variant_wrapper; + using variant_wrapper::visit; + + static simple_expression parse(std::string_view& sv) { + sv = bpt::trim_view(sv); + auto tok = next_token(sv); + auto license = find_with_id(tok); + if (!license) { + BOOST_LEAF_THROW_EXCEPTION( + e_bad_spdx_expression{neo::ufmt("No such SPDX license '{}'", tok)}); + } + sv.remove_prefix(tok.size()); + tok = next_token(sv); + if (tok == "+") { + sv.remove_prefix(1); + return license_id_plus{*license}; + } else { + return just_license_id{*license}; + } + } +}; + +struct and_license { + std::shared_ptr left; + std::shared_ptr right; +}; + +struct or_license { + std::shared_ptr left; + std::shared_ptr right; +}; + +struct with_exception { + simple_expression license; + spdx_exception_info exception; +}; + +struct paren_grouped { + std::shared_ptr expr; +}; + +struct compound_expression : bpt::variant_wrapper { + + using variant_wrapper::variant_wrapper; + using variant_wrapper::visit; + + static compound_expression parse(std::string_view& sv) { + sv = bpt::trim_view(sv); + if (next_token(sv) == "(") { + sv.remove_prefix(1); + auto inner = parse(sv); + sv = bpt::trim_view(sv); + if (next_token(sv) != ")") { + BOOST_LEAF_THROW_EXCEPTION(e_bad_spdx_expression{"Missing closing parenthesis"}); + } + sv.remove_prefix(1); + return paren_grouped{neo::copy_shared(inner)}; + } + auto simple = simple_expression::parse(sv); + sv = bpt::trim_view(sv); + auto next = next_token(sv); + if (next == "OR") { + sv.remove_prefix(2); + auto rhs = compound_expression::parse(sv); + return or_license{neo::copy_shared(compound_expression{simple}), neo::copy_shared(rhs)}; + } else if (next == "AND") { + sv.remove_prefix(3); + auto rhs = compound_expression::parse(sv); + return and_license{neo::copy_shared(compound_expression{simple}), + neo::copy_shared(rhs)}; + } else if (next == "WITH") { + sv.remove_prefix(4); + auto exc_id = next_token(sv); + auto exc = find_with_id(exc_id); + if (!exc) { + BOOST_LEAF_THROW_EXCEPTION(e_bad_spdx_expression{ + neo::ufmt("No such SPDX license exception '{}'", exc_id)}); + } + sv.remove_prefix(exc_id.size()); + return with_exception{std::move(simple), *exc}; + } + return simple; + } +}; + +struct license_to_string_fn { + std::string str(just_license_id& l) { return l.license.id; } + std::string str(license_id_plus& l) { return l.license.id + "+"; } + + std::string str(and_license& a) { return neo::ufmt("{} AND {}", str(*a.left), str(*a.right)); } + + std::string str(or_license& o) { return neo::ufmt("{} OR {}", str(*o.left), str(*o.right)); } + + std::string str(paren_grouped& o) { return neo::ufmt("({})", str(*o.expr)); } + + std::string str(simple_expression& e) { return e.visit(*this); } + std::string str(compound_expression& e) { return e.visit(*this); } + + std::string str(with_exception& e) { + return neo::ufmt("{} WITH {}", str(e.license), str(e.exception)); + } + + std::string str(spdx_exception_info& e) { return e.id; } + + std::string operator()(auto& l) { return str(l); } +}; + +} // namespace + +struct spdx_license_expression::impl { + compound_expression expr; +}; + +spdx_license_expression spdx_license_expression::parse(std::string_view const sv) { + BPT_E_SCOPE(e_spdx_license_str{std::string(sv)}); + + auto sv1 = sv; + auto r = compound_expression::parse(sv1); + if (!bpt::trim_view(sv1).empty()) { + BOOST_LEAF_THROW_EXCEPTION( + e_bad_spdx_expression{neo::ufmt("Unknown trailing string content '{}'", sv1)}, r); + } + return spdx_license_expression{neo::copy_shared(impl{r})}; +} + +std::string spdx_license_expression::to_string() const noexcept { + return _impl->expr.visit(license_to_string_fn{}); +} diff --git a/src/bpt/project/spdx.hpp b/src/bpt/project/spdx.hpp new file mode 100644 index 00000000..ee27db55 --- /dev/null +++ b/src/bpt/project/spdx.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include + +namespace bpt { + +struct e_spdx_license_str { + std::string value; +}; + +struct e_bad_spdx_expression { + std::string value; +}; + +struct spdx_license_info { + std::string id; + std::string name; + + static const std::span all; +}; + +struct spdx_exception_info { + std::string id; + std::string name; + + static const std::span all; +}; + +class spdx_license_expression { + struct impl; + + std::shared_ptr _impl; + + explicit spdx_license_expression(std::shared_ptr i) noexcept + : _impl(i) {} + +public: + static spdx_license_expression parse(std::string_view sv); + + std::string to_string() const noexcept; +}; + +} // namespace bpt diff --git a/src/bpt/project/spdx.inl b/src/bpt/project/spdx.inl new file mode 100644 index 00000000..b26a4f12 --- /dev/null +++ b/src/bpt/project/spdx.inl @@ -0,0 +1,482 @@ +SPDX_LICENSE(R"_(0BSD)_", R"_(BSD Zero Clause License)_") +SPDX_LICENSE(R"_(AAL)_", R"_(Attribution Assurance License)_") +SPDX_LICENSE(R"_(ADSL)_", R"_(Amazon Digital Services License)_") +SPDX_LICENSE(R"_(AFL-1.1)_", R"_(Academic Free License v1.1)_") +SPDX_LICENSE(R"_(AFL-1.2)_", R"_(Academic Free License v1.2)_") +SPDX_LICENSE(R"_(AFL-2.0)_", R"_(Academic Free License v2.0)_") +SPDX_LICENSE(R"_(AFL-2.1)_", R"_(Academic Free License v2.1)_") +SPDX_LICENSE(R"_(AFL-3.0)_", R"_(Academic Free License v3.0)_") +SPDX_LICENSE(R"_(AGPL-1.0)_", R"_(Affero General Public License v1.0)_") +SPDX_LICENSE(R"_(AGPL-1.0-only)_", R"_(Affero General Public License v1.0 only)_") +SPDX_LICENSE(R"_(AGPL-1.0-or-later)_", R"_(Affero General Public License v1.0 or later)_") +SPDX_LICENSE(R"_(AGPL-3.0)_", R"_(GNU Affero General Public License v3.0)_") +SPDX_LICENSE(R"_(AGPL-3.0-only)_", R"_(GNU Affero General Public License v3.0 only)_") +SPDX_LICENSE(R"_(AGPL-3.0-or-later)_", R"_(GNU Affero General Public License v3.0 or later)_") +SPDX_LICENSE(R"_(AMDPLPA)_", R"_(AMD's plpa_map.c License)_") +SPDX_LICENSE(R"_(AML)_", R"_(Apple MIT License)_") +SPDX_LICENSE(R"_(AMPAS)_", R"_(Academy of Motion Picture Arts and Sciences BSD)_") +SPDX_LICENSE(R"_(ANTLR-PD)_", R"_(ANTLR Software Rights Notice)_") +SPDX_LICENSE(R"_(ANTLR-PD-fallback)_", R"_(ANTLR Software Rights Notice with license fallback)_") +SPDX_LICENSE(R"_(APAFML)_", R"_(Adobe Postscript AFM License)_") +SPDX_LICENSE(R"_(APL-1.0)_", R"_(Adaptive Public License 1.0)_") +SPDX_LICENSE(R"_(APSL-1.0)_", R"_(Apple Public Source License 1.0)_") +SPDX_LICENSE(R"_(APSL-1.1)_", R"_(Apple Public Source License 1.1)_") +SPDX_LICENSE(R"_(APSL-1.2)_", R"_(Apple Public Source License 1.2)_") +SPDX_LICENSE(R"_(APSL-2.0)_", R"_(Apple Public Source License 2.0)_") +SPDX_LICENSE(R"_(Abstyles)_", R"_(Abstyles License)_") +SPDX_LICENSE(R"_(Adobe-2006)_", R"_(Adobe Systems Incorporated Source Code License Agreement)_") +SPDX_LICENSE(R"_(Adobe-Glyph)_", R"_(Adobe Glyph List License)_") +SPDX_LICENSE(R"_(Afmparse)_", R"_(Afmparse License)_") +SPDX_LICENSE(R"_(Aladdin)_", R"_(Aladdin Free Public License)_") +SPDX_LICENSE(R"_(Apache-1.0)_", R"_(Apache License 1.0)_") +SPDX_LICENSE(R"_(Apache-1.1)_", R"_(Apache License 1.1)_") +SPDX_LICENSE(R"_(Apache-2.0)_", R"_(Apache License 2.0)_") +SPDX_LICENSE(R"_(App-s2p)_", R"_(App::s2p License)_") +SPDX_LICENSE(R"_(Artistic-1.0)_", R"_(Artistic License 1.0)_") +SPDX_LICENSE(R"_(Artistic-1.0-Perl)_", R"_(Artistic License 1.0 (Perl))_") +SPDX_LICENSE(R"_(Artistic-1.0-cl8)_", R"_(Artistic License 1.0 w/clause 8)_") +SPDX_LICENSE(R"_(Artistic-2.0)_", R"_(Artistic License 2.0)_") +SPDX_LICENSE(R"_(BSD-1-Clause)_", R"_(BSD 1-Clause License)_") +SPDX_LICENSE(R"_(BSD-2-Clause)_", R"_(BSD 2-Clause "Simplified" License)_") +SPDX_LICENSE(R"_(BSD-2-Clause-FreeBSD)_", R"_(BSD 2-Clause FreeBSD License)_") +SPDX_LICENSE(R"_(BSD-2-Clause-NetBSD)_", R"_(BSD 2-Clause NetBSD License)_") +SPDX_LICENSE(R"_(BSD-2-Clause-Patent)_", R"_(BSD-2-Clause Plus Patent License)_") +SPDX_LICENSE(R"_(BSD-2-Clause-Views)_", R"_(BSD 2-Clause with views sentence)_") +SPDX_LICENSE(R"_(BSD-3-Clause)_", R"_(BSD 3-Clause "New" or "Revised" License)_") +SPDX_LICENSE(R"_(BSD-3-Clause-Attribution)_", R"_(BSD with attribution)_") +SPDX_LICENSE(R"_(BSD-3-Clause-Clear)_", R"_(BSD 3-Clause Clear License)_") +SPDX_LICENSE(R"_(BSD-3-Clause-LBNL)_", R"_(Lawrence Berkeley National Labs BSD variant license)_") +SPDX_LICENSE(R"_(BSD-3-Clause-Modification)_", R"_(BSD 3-Clause Modification)_") +SPDX_LICENSE(R"_(BSD-3-Clause-No-Military-License)_", R"_(BSD 3-Clause No Military License)_") +SPDX_LICENSE(R"_(BSD-3-Clause-No-Nuclear-License)_", R"_(BSD 3-Clause No Nuclear License)_") +SPDX_LICENSE(R"_(BSD-3-Clause-No-Nuclear-License-2014)_", R"_(BSD 3-Clause No Nuclear License 2014)_") +SPDX_LICENSE(R"_(BSD-3-Clause-No-Nuclear-Warranty)_", R"_(BSD 3-Clause No Nuclear Warranty)_") +SPDX_LICENSE(R"_(BSD-3-Clause-Open-MPI)_", R"_(BSD 3-Clause Open MPI variant)_") +SPDX_LICENSE(R"_(BSD-4-Clause)_", R"_(BSD 4-Clause "Original" or "Old" License)_") +SPDX_LICENSE(R"_(BSD-4-Clause-Shortened)_", R"_(BSD 4 Clause Shortened)_") +SPDX_LICENSE(R"_(BSD-4-Clause-UC)_", R"_(BSD-4-Clause (University of California-Specific))_") +SPDX_LICENSE(R"_(BSD-Protection)_", R"_(BSD Protection License)_") +SPDX_LICENSE(R"_(BSD-Source-Code)_", R"_(BSD Source Code Attribution)_") +SPDX_LICENSE(R"_(BSL-1.0)_", R"_(Boost Software License 1.0)_") +SPDX_LICENSE(R"_(BUSL-1.1)_", R"_(Business Source License 1.1)_") +SPDX_LICENSE(R"_(Bahyph)_", R"_(Bahyph License)_") +SPDX_LICENSE(R"_(Barr)_", R"_(Barr License)_") +SPDX_LICENSE(R"_(Beerware)_", R"_(Beerware License)_") +SPDX_LICENSE(R"_(BitTorrent-1.0)_", R"_(BitTorrent Open Source License v1.0)_") +SPDX_LICENSE(R"_(BitTorrent-1.1)_", R"_(BitTorrent Open Source License v1.1)_") +SPDX_LICENSE(R"_(BlueOak-1.0.0)_", R"_(Blue Oak Model License 1.0.0)_") +SPDX_LICENSE(R"_(Borceux)_", R"_(Borceux license)_") +SPDX_LICENSE(R"_(C-UDA-1.0)_", R"_(Computational Use of Data Agreement v1.0)_") +SPDX_LICENSE(R"_(CAL-1.0)_", R"_(Cryptographic Autonomy License 1.0)_") +SPDX_LICENSE(R"_(CAL-1.0-Combined-Work-Exception)_", R"_(Cryptographic Autonomy License 1.0 (Combined Work Exception))_") +SPDX_LICENSE(R"_(CATOSL-1.1)_", R"_(Computer Associates Trusted Open Source License 1.1)_") +SPDX_LICENSE(R"_(CC-BY-1.0)_", R"_(Creative Commons Attribution 1.0 Generic)_") +SPDX_LICENSE(R"_(CC-BY-2.0)_", R"_(Creative Commons Attribution 2.0 Generic)_") +SPDX_LICENSE(R"_(CC-BY-2.5)_", R"_(Creative Commons Attribution 2.5 Generic)_") +SPDX_LICENSE(R"_(CC-BY-2.5-AU)_", R"_(Creative Commons Attribution 2.5 Australia)_") +SPDX_LICENSE(R"_(CC-BY-3.0)_", R"_(Creative Commons Attribution 3.0 Unported)_") +SPDX_LICENSE(R"_(CC-BY-3.0-AT)_", R"_(Creative Commons Attribution 3.0 Austria)_") +SPDX_LICENSE(R"_(CC-BY-3.0-DE)_", R"_(Creative Commons Attribution 3.0 Germany)_") +SPDX_LICENSE(R"_(CC-BY-3.0-NL)_", R"_(Creative Commons Attribution 3.0 Netherlands)_") +SPDX_LICENSE(R"_(CC-BY-3.0-US)_", R"_(Creative Commons Attribution 3.0 United States)_") +SPDX_LICENSE(R"_(CC-BY-4.0)_", R"_(Creative Commons Attribution 4.0 International)_") +SPDX_LICENSE(R"_(CC-BY-NC-1.0)_", R"_(Creative Commons Attribution Non Commercial 1.0 Generic)_") +SPDX_LICENSE(R"_(CC-BY-NC-2.0)_", R"_(Creative Commons Attribution Non Commercial 2.0 Generic)_") +SPDX_LICENSE(R"_(CC-BY-NC-2.5)_", R"_(Creative Commons Attribution Non Commercial 2.5 Generic)_") +SPDX_LICENSE(R"_(CC-BY-NC-3.0)_", R"_(Creative Commons Attribution Non Commercial 3.0 Unported)_") +SPDX_LICENSE(R"_(CC-BY-NC-3.0-DE)_", R"_(Creative Commons Attribution Non Commercial 3.0 Germany)_") +SPDX_LICENSE(R"_(CC-BY-NC-4.0)_", R"_(Creative Commons Attribution Non Commercial 4.0 International)_") +SPDX_LICENSE(R"_(CC-BY-NC-ND-1.0)_", R"_(Creative Commons Attribution Non Commercial No Derivatives 1.0 Generic)_") +SPDX_LICENSE(R"_(CC-BY-NC-ND-2.0)_", R"_(Creative Commons Attribution Non Commercial No Derivatives 2.0 Generic)_") +SPDX_LICENSE(R"_(CC-BY-NC-ND-2.5)_", R"_(Creative Commons Attribution Non Commercial No Derivatives 2.5 Generic)_") +SPDX_LICENSE(R"_(CC-BY-NC-ND-3.0)_", R"_(Creative Commons Attribution Non Commercial No Derivatives 3.0 Unported)_") +SPDX_LICENSE(R"_(CC-BY-NC-ND-3.0-DE)_", R"_(Creative Commons Attribution Non Commercial No Derivatives 3.0 Germany)_") +SPDX_LICENSE(R"_(CC-BY-NC-ND-3.0-IGO)_", R"_(Creative Commons Attribution Non Commercial No Derivatives 3.0 IGO)_") +SPDX_LICENSE(R"_(CC-BY-NC-ND-4.0)_", R"_(Creative Commons Attribution Non Commercial No Derivatives 4.0 International)_") +SPDX_LICENSE(R"_(CC-BY-NC-SA-1.0)_", R"_(Creative Commons Attribution Non Commercial Share Alike 1.0 Generic)_") +SPDX_LICENSE(R"_(CC-BY-NC-SA-2.0)_", R"_(Creative Commons Attribution Non Commercial Share Alike 2.0 Generic)_") +SPDX_LICENSE(R"_(CC-BY-NC-SA-2.0-FR)_", R"_(Creative Commons Attribution-NonCommercial-ShareAlike 2.0 France)_") +SPDX_LICENSE(R"_(CC-BY-NC-SA-2.0-UK)_", R"_(Creative Commons Attribution Non Commercial Share Alike 2.0 England and Wales)_") +SPDX_LICENSE(R"_(CC-BY-NC-SA-2.5)_", R"_(Creative Commons Attribution Non Commercial Share Alike 2.5 Generic)_") +SPDX_LICENSE(R"_(CC-BY-NC-SA-3.0)_", R"_(Creative Commons Attribution Non Commercial Share Alike 3.0 Unported)_") +SPDX_LICENSE(R"_(CC-BY-NC-SA-3.0-DE)_", R"_(Creative Commons Attribution Non Commercial Share Alike 3.0 Germany)_") +SPDX_LICENSE(R"_(CC-BY-NC-SA-3.0-IGO)_", R"_(Creative Commons Attribution Non Commercial Share Alike 3.0 IGO)_") +SPDX_LICENSE(R"_(CC-BY-NC-SA-4.0)_", R"_(Creative Commons Attribution Non Commercial Share Alike 4.0 International)_") +SPDX_LICENSE(R"_(CC-BY-ND-1.0)_", R"_(Creative Commons Attribution No Derivatives 1.0 Generic)_") +SPDX_LICENSE(R"_(CC-BY-ND-2.0)_", R"_(Creative Commons Attribution No Derivatives 2.0 Generic)_") +SPDX_LICENSE(R"_(CC-BY-ND-2.5)_", R"_(Creative Commons Attribution No Derivatives 2.5 Generic)_") +SPDX_LICENSE(R"_(CC-BY-ND-3.0)_", R"_(Creative Commons Attribution No Derivatives 3.0 Unported)_") +SPDX_LICENSE(R"_(CC-BY-ND-3.0-DE)_", R"_(Creative Commons Attribution No Derivatives 3.0 Germany)_") +SPDX_LICENSE(R"_(CC-BY-ND-4.0)_", R"_(Creative Commons Attribution No Derivatives 4.0 International)_") +SPDX_LICENSE(R"_(CC-BY-SA-1.0)_", R"_(Creative Commons Attribution Share Alike 1.0 Generic)_") +SPDX_LICENSE(R"_(CC-BY-SA-2.0)_", R"_(Creative Commons Attribution Share Alike 2.0 Generic)_") +SPDX_LICENSE(R"_(CC-BY-SA-2.0-UK)_", R"_(Creative Commons Attribution Share Alike 2.0 England and Wales)_") +SPDX_LICENSE(R"_(CC-BY-SA-2.1-JP)_", R"_(Creative Commons Attribution Share Alike 2.1 Japan)_") +SPDX_LICENSE(R"_(CC-BY-SA-2.5)_", R"_(Creative Commons Attribution Share Alike 2.5 Generic)_") +SPDX_LICENSE(R"_(CC-BY-SA-3.0)_", R"_(Creative Commons Attribution Share Alike 3.0 Unported)_") +SPDX_LICENSE(R"_(CC-BY-SA-3.0-AT)_", R"_(Creative Commons Attribution Share Alike 3.0 Austria)_") +SPDX_LICENSE(R"_(CC-BY-SA-3.0-DE)_", R"_(Creative Commons Attribution Share Alike 3.0 Germany)_") +SPDX_LICENSE(R"_(CC-BY-SA-4.0)_", R"_(Creative Commons Attribution Share Alike 4.0 International)_") +SPDX_LICENSE(R"_(CC-PDDC)_", R"_(Creative Commons Public Domain Dedication and Certification)_") +SPDX_LICENSE(R"_(CC0-1.0)_", R"_(Creative Commons Zero v1.0 Universal)_") +SPDX_LICENSE(R"_(CDDL-1.0)_", R"_(Common Development and Distribution License 1.0)_") +SPDX_LICENSE(R"_(CDDL-1.1)_", R"_(Common Development and Distribution License 1.1)_") +SPDX_LICENSE(R"_(CDL-1.0)_", R"_(Common Documentation License 1.0)_") +SPDX_LICENSE(R"_(CDLA-Permissive-1.0)_", R"_(Community Data License Agreement Permissive 1.0)_") +SPDX_LICENSE(R"_(CDLA-Permissive-2.0)_", R"_(Community Data License Agreement Permissive 2.0)_") +SPDX_LICENSE(R"_(CDLA-Sharing-1.0)_", R"_(Community Data License Agreement Sharing 1.0)_") +SPDX_LICENSE(R"_(CECILL-1.0)_", R"_(CeCILL Free Software License Agreement v1.0)_") +SPDX_LICENSE(R"_(CECILL-1.1)_", R"_(CeCILL Free Software License Agreement v1.1)_") +SPDX_LICENSE(R"_(CECILL-2.0)_", R"_(CeCILL Free Software License Agreement v2.0)_") +SPDX_LICENSE(R"_(CECILL-2.1)_", R"_(CeCILL Free Software License Agreement v2.1)_") +SPDX_LICENSE(R"_(CECILL-B)_", R"_(CeCILL-B Free Software License Agreement)_") +SPDX_LICENSE(R"_(CECILL-C)_", R"_(CeCILL-C Free Software License Agreement)_") +SPDX_LICENSE(R"_(CERN-OHL-1.1)_", R"_(CERN Open Hardware Licence v1.1)_") +SPDX_LICENSE(R"_(CERN-OHL-1.2)_", R"_(CERN Open Hardware Licence v1.2)_") +SPDX_LICENSE(R"_(CERN-OHL-P-2.0)_", R"_(CERN Open Hardware Licence Version 2 - Permissive)_") +SPDX_LICENSE(R"_(CERN-OHL-S-2.0)_", R"_(CERN Open Hardware Licence Version 2 - Strongly Reciprocal)_") +SPDX_LICENSE(R"_(CERN-OHL-W-2.0)_", R"_(CERN Open Hardware Licence Version 2 - Weakly Reciprocal)_") +SPDX_LICENSE(R"_(CNRI-Jython)_", R"_(CNRI Jython License)_") +SPDX_LICENSE(R"_(CNRI-Python)_", R"_(CNRI Python License)_") +SPDX_LICENSE(R"_(CNRI-Python-GPL-Compatible)_", R"_(CNRI Python Open Source GPL Compatible License Agreement)_") +SPDX_LICENSE(R"_(COIL-1.0)_", R"_(Copyfree Open Innovation License)_") +SPDX_LICENSE(R"_(CPAL-1.0)_", R"_(Common Public Attribution License 1.0)_") +SPDX_LICENSE(R"_(CPL-1.0)_", R"_(Common Public License 1.0)_") +SPDX_LICENSE(R"_(CPOL-1.02)_", R"_(Code Project Open License 1.02)_") +SPDX_LICENSE(R"_(CUA-OPL-1.0)_", R"_(CUA Office Public License v1.0)_") +SPDX_LICENSE(R"_(Caldera)_", R"_(Caldera License)_") +SPDX_LICENSE(R"_(ClArtistic)_", R"_(Clarified Artistic License)_") +SPDX_LICENSE(R"_(Community-Spec-1.0)_", R"_(Community Specification License 1.0)_") +SPDX_LICENSE(R"_(Condor-1.1)_", R"_(Condor Public License v1.1)_") +SPDX_LICENSE(R"_(Crossword)_", R"_(Crossword License)_") +SPDX_LICENSE(R"_(CrystalStacker)_", R"_(CrystalStacker License)_") +SPDX_LICENSE(R"_(Cube)_", R"_(Cube License)_") +SPDX_LICENSE(R"_(D-FSL-1.0)_", R"_(Deutsche Freie Software Lizenz)_") +SPDX_LICENSE(R"_(DL-DE-BY-2.0)_", R"_(Data licence Germany – attribution – version 2.0)_") +SPDX_LICENSE(R"_(DOC)_", R"_(DOC License)_") +SPDX_LICENSE(R"_(DRL-1.0)_", R"_(Detection Rule License 1.0)_") +SPDX_LICENSE(R"_(DSDP)_", R"_(DSDP License)_") +SPDX_LICENSE(R"_(Dotseqn)_", R"_(Dotseqn License)_") +SPDX_LICENSE(R"_(ECL-1.0)_", R"_(Educational Community License v1.0)_") +SPDX_LICENSE(R"_(ECL-2.0)_", R"_(Educational Community License v2.0)_") +SPDX_LICENSE(R"_(EFL-1.0)_", R"_(Eiffel Forum License v1.0)_") +SPDX_LICENSE(R"_(EFL-2.0)_", R"_(Eiffel Forum License v2.0)_") +SPDX_LICENSE(R"_(EPICS)_", R"_(EPICS Open License)_") +SPDX_LICENSE(R"_(EPL-1.0)_", R"_(Eclipse Public License 1.0)_") +SPDX_LICENSE(R"_(EPL-2.0)_", R"_(Eclipse Public License 2.0)_") +SPDX_LICENSE(R"_(EUDatagrid)_", R"_(EU DataGrid Software License)_") +SPDX_LICENSE(R"_(EUPL-1.0)_", R"_(European Union Public License 1.0)_") +SPDX_LICENSE(R"_(EUPL-1.1)_", R"_(European Union Public License 1.1)_") +SPDX_LICENSE(R"_(EUPL-1.2)_", R"_(European Union Public License 1.2)_") +SPDX_LICENSE(R"_(Elastic-2.0)_", R"_(Elastic License 2.0)_") +SPDX_LICENSE(R"_(Entessa)_", R"_(Entessa Public License v1.0)_") +SPDX_LICENSE(R"_(ErlPL-1.1)_", R"_(Erlang Public License v1.1)_") +SPDX_LICENSE(R"_(Eurosym)_", R"_(Eurosym License)_") +SPDX_LICENSE(R"_(FDK-AAC)_", R"_(Fraunhofer FDK AAC Codec Library)_") +SPDX_LICENSE(R"_(FSFAP)_", R"_(FSF All Permissive License)_") +SPDX_LICENSE(R"_(FSFUL)_", R"_(FSF Unlimited License)_") +SPDX_LICENSE(R"_(FSFULLR)_", R"_(FSF Unlimited License (with License Retention))_") +SPDX_LICENSE(R"_(FTL)_", R"_(Freetype Project License)_") +SPDX_LICENSE(R"_(Fair)_", R"_(Fair License)_") +SPDX_LICENSE(R"_(Frameworx-1.0)_", R"_(Frameworx Open License 1.0)_") +SPDX_LICENSE(R"_(FreeBSD-DOC)_", R"_(FreeBSD Documentation License)_") +SPDX_LICENSE(R"_(FreeImage)_", R"_(FreeImage Public License v1.0)_") +SPDX_LICENSE(R"_(GD)_", R"_(GD License)_") +SPDX_LICENSE(R"_(GFDL-1.1)_", R"_(GNU Free Documentation License v1.1)_") +SPDX_LICENSE(R"_(GFDL-1.1-invariants-only)_", R"_(GNU Free Documentation License v1.1 only - invariants)_") +SPDX_LICENSE(R"_(GFDL-1.1-invariants-or-later)_", R"_(GNU Free Documentation License v1.1 or later - invariants)_") +SPDX_LICENSE(R"_(GFDL-1.1-no-invariants-only)_", R"_(GNU Free Documentation License v1.1 only - no invariants)_") +SPDX_LICENSE(R"_(GFDL-1.1-no-invariants-or-later)_", R"_(GNU Free Documentation License v1.1 or later - no invariants)_") +SPDX_LICENSE(R"_(GFDL-1.1-only)_", R"_(GNU Free Documentation License v1.1 only)_") +SPDX_LICENSE(R"_(GFDL-1.1-or-later)_", R"_(GNU Free Documentation License v1.1 or later)_") +SPDX_LICENSE(R"_(GFDL-1.2)_", R"_(GNU Free Documentation License v1.2)_") +SPDX_LICENSE(R"_(GFDL-1.2-invariants-only)_", R"_(GNU Free Documentation License v1.2 only - invariants)_") +SPDX_LICENSE(R"_(GFDL-1.2-invariants-or-later)_", R"_(GNU Free Documentation License v1.2 or later - invariants)_") +SPDX_LICENSE(R"_(GFDL-1.2-no-invariants-only)_", R"_(GNU Free Documentation License v1.2 only - no invariants)_") +SPDX_LICENSE(R"_(GFDL-1.2-no-invariants-or-later)_", R"_(GNU Free Documentation License v1.2 or later - no invariants)_") +SPDX_LICENSE(R"_(GFDL-1.2-only)_", R"_(GNU Free Documentation License v1.2 only)_") +SPDX_LICENSE(R"_(GFDL-1.2-or-later)_", R"_(GNU Free Documentation License v1.2 or later)_") +SPDX_LICENSE(R"_(GFDL-1.3)_", R"_(GNU Free Documentation License v1.3)_") +SPDX_LICENSE(R"_(GFDL-1.3-invariants-only)_", R"_(GNU Free Documentation License v1.3 only - invariants)_") +SPDX_LICENSE(R"_(GFDL-1.3-invariants-or-later)_", R"_(GNU Free Documentation License v1.3 or later - invariants)_") +SPDX_LICENSE(R"_(GFDL-1.3-no-invariants-only)_", R"_(GNU Free Documentation License v1.3 only - no invariants)_") +SPDX_LICENSE(R"_(GFDL-1.3-no-invariants-or-later)_", R"_(GNU Free Documentation License v1.3 or later - no invariants)_") +SPDX_LICENSE(R"_(GFDL-1.3-only)_", R"_(GNU Free Documentation License v1.3 only)_") +SPDX_LICENSE(R"_(GFDL-1.3-or-later)_", R"_(GNU Free Documentation License v1.3 or later)_") +SPDX_LICENSE(R"_(GL2PS)_", R"_(GL2PS License)_") +SPDX_LICENSE(R"_(GLWTPL)_", R"_(Good Luck With That Public License)_") +SPDX_LICENSE(R"_(GPL-1.0)_", R"_(GNU General Public License v1.0 only)_") +SPDX_LICENSE(R"_(GPL-1.0+)_", R"_(GNU General Public License v1.0 or later)_") +SPDX_LICENSE(R"_(GPL-1.0-only)_", R"_(GNU General Public License v1.0 only)_") +SPDX_LICENSE(R"_(GPL-1.0-or-later)_", R"_(GNU General Public License v1.0 or later)_") +SPDX_LICENSE(R"_(GPL-2.0)_", R"_(GNU General Public License v2.0 only)_") +SPDX_LICENSE(R"_(GPL-2.0+)_", R"_(GNU General Public License v2.0 or later)_") +SPDX_LICENSE(R"_(GPL-2.0-only)_", R"_(GNU General Public License v2.0 only)_") +SPDX_LICENSE(R"_(GPL-2.0-or-later)_", R"_(GNU General Public License v2.0 or later)_") +SPDX_LICENSE(R"_(GPL-2.0-with-GCC-exception)_", R"_(GNU General Public License v2.0 w/GCC Runtime Library exception)_") +SPDX_LICENSE(R"_(GPL-2.0-with-autoconf-exception)_", R"_(GNU General Public License v2.0 w/Autoconf exception)_") +SPDX_LICENSE(R"_(GPL-2.0-with-bison-exception)_", R"_(GNU General Public License v2.0 w/Bison exception)_") +SPDX_LICENSE(R"_(GPL-2.0-with-classpath-exception)_", R"_(GNU General Public License v2.0 w/Classpath exception)_") +SPDX_LICENSE(R"_(GPL-2.0-with-font-exception)_", R"_(GNU General Public License v2.0 w/Font exception)_") +SPDX_LICENSE(R"_(GPL-3.0)_", R"_(GNU General Public License v3.0 only)_") +SPDX_LICENSE(R"_(GPL-3.0+)_", R"_(GNU General Public License v3.0 or later)_") +SPDX_LICENSE(R"_(GPL-3.0-only)_", R"_(GNU General Public License v3.0 only)_") +SPDX_LICENSE(R"_(GPL-3.0-or-later)_", R"_(GNU General Public License v3.0 or later)_") +SPDX_LICENSE(R"_(GPL-3.0-with-GCC-exception)_", R"_(GNU General Public License v3.0 w/GCC Runtime Library exception)_") +SPDX_LICENSE(R"_(GPL-3.0-with-autoconf-exception)_", R"_(GNU General Public License v3.0 w/Autoconf exception)_") +SPDX_LICENSE(R"_(Giftware)_", R"_(Giftware License)_") +SPDX_LICENSE(R"_(Glide)_", R"_(3dfx Glide License)_") +SPDX_LICENSE(R"_(Glulxe)_", R"_(Glulxe License)_") +SPDX_LICENSE(R"_(HPND)_", R"_(Historical Permission Notice and Disclaimer)_") +SPDX_LICENSE(R"_(HPND-sell-variant)_", R"_(Historical Permission Notice and Disclaimer - sell variant)_") +SPDX_LICENSE(R"_(HTMLTIDY)_", R"_(HTML Tidy License)_") +SPDX_LICENSE(R"_(HaskellReport)_", R"_(Haskell Language Report License)_") +SPDX_LICENSE(R"_(Hippocratic-2.1)_", R"_(Hippocratic License 2.1)_") +SPDX_LICENSE(R"_(IBM-pibs)_", R"_(IBM PowerPC Initialization and Boot Software)_") +SPDX_LICENSE(R"_(ICU)_", R"_(ICU License)_") +SPDX_LICENSE(R"_(IJG)_", R"_(Independent JPEG Group License)_") +SPDX_LICENSE(R"_(IPA)_", R"_(IPA Font License)_") +SPDX_LICENSE(R"_(IPL-1.0)_", R"_(IBM Public License v1.0)_") +SPDX_LICENSE(R"_(ISC)_", R"_(ISC License)_") +SPDX_LICENSE(R"_(ImageMagick)_", R"_(ImageMagick License)_") +SPDX_LICENSE(R"_(Imlib2)_", R"_(Imlib2 License)_") +SPDX_LICENSE(R"_(Info-ZIP)_", R"_(Info-ZIP License)_") +SPDX_LICENSE(R"_(Intel)_", R"_(Intel Open Source License)_") +SPDX_LICENSE(R"_(Intel-ACPI)_", R"_(Intel ACPI Software License Agreement)_") +SPDX_LICENSE(R"_(Interbase-1.0)_", R"_(Interbase Public License v1.0)_") +SPDX_LICENSE(R"_(JPNIC)_", R"_(Japan Network Information Center License)_") +SPDX_LICENSE(R"_(JSON)_", R"_(JSON License)_") +SPDX_LICENSE(R"_(JasPer-2.0)_", R"_(JasPer License)_") +SPDX_LICENSE(R"_(LAL-1.2)_", R"_(Licence Art Libre 1.2)_") +SPDX_LICENSE(R"_(LAL-1.3)_", R"_(Licence Art Libre 1.3)_") +SPDX_LICENSE(R"_(LGPL-2.0)_", R"_(GNU Library General Public License v2 only)_") +SPDX_LICENSE(R"_(LGPL-2.0+)_", R"_(GNU Library General Public License v2 or later)_") +SPDX_LICENSE(R"_(LGPL-2.0-only)_", R"_(GNU Library General Public License v2 only)_") +SPDX_LICENSE(R"_(LGPL-2.0-or-later)_", R"_(GNU Library General Public License v2 or later)_") +SPDX_LICENSE(R"_(LGPL-2.1)_", R"_(GNU Lesser General Public License v2.1 only)_") +SPDX_LICENSE(R"_(LGPL-2.1+)_", R"_(GNU Library General Public License v2.1 or later)_") +SPDX_LICENSE(R"_(LGPL-2.1-only)_", R"_(GNU Lesser General Public License v2.1 only)_") +SPDX_LICENSE(R"_(LGPL-2.1-or-later)_", R"_(GNU Lesser General Public License v2.1 or later)_") +SPDX_LICENSE(R"_(LGPL-3.0)_", R"_(GNU Lesser General Public License v3.0 only)_") +SPDX_LICENSE(R"_(LGPL-3.0+)_", R"_(GNU Lesser General Public License v3.0 or later)_") +SPDX_LICENSE(R"_(LGPL-3.0-only)_", R"_(GNU Lesser General Public License v3.0 only)_") +SPDX_LICENSE(R"_(LGPL-3.0-or-later)_", R"_(GNU Lesser General Public License v3.0 or later)_") +SPDX_LICENSE(R"_(LGPLLR)_", R"_(Lesser General Public License For Linguistic Resources)_") +SPDX_LICENSE(R"_(LPL-1.0)_", R"_(Lucent Public License Version 1.0)_") +SPDX_LICENSE(R"_(LPL-1.02)_", R"_(Lucent Public License v1.02)_") +SPDX_LICENSE(R"_(LPPL-1.0)_", R"_(LaTeX Project Public License v1.0)_") +SPDX_LICENSE(R"_(LPPL-1.1)_", R"_(LaTeX Project Public License v1.1)_") +SPDX_LICENSE(R"_(LPPL-1.2)_", R"_(LaTeX Project Public License v1.2)_") +SPDX_LICENSE(R"_(LPPL-1.3a)_", R"_(LaTeX Project Public License v1.3a)_") +SPDX_LICENSE(R"_(LPPL-1.3c)_", R"_(LaTeX Project Public License v1.3c)_") +SPDX_LICENSE(R"_(Latex2e)_", R"_(Latex2e License)_") +SPDX_LICENSE(R"_(Leptonica)_", R"_(Leptonica License)_") +SPDX_LICENSE(R"_(LiLiQ-P-1.1)_", R"_(Licence Libre du Québec – Permissive version 1.1)_") +SPDX_LICENSE(R"_(LiLiQ-R-1.1)_", R"_(Licence Libre du Québec – Réciprocité version 1.1)_") +SPDX_LICENSE(R"_(LiLiQ-Rplus-1.1)_", R"_(Licence Libre du Québec – Réciprocité forte version 1.1)_") +SPDX_LICENSE(R"_(Libpng)_", R"_(libpng License)_") +SPDX_LICENSE(R"_(Linux-OpenIB)_", R"_(Linux Kernel Variant of OpenIB.org license)_") +SPDX_LICENSE(R"_(Linux-man-pages-copyleft)_", R"_(Linux man-pages Copyleft)_") +SPDX_LICENSE(R"_(MIT)_", R"_(MIT License)_") +SPDX_LICENSE(R"_(MIT-0)_", R"_(MIT No Attribution)_") +SPDX_LICENSE(R"_(MIT-CMU)_", R"_(CMU License)_") +SPDX_LICENSE(R"_(MIT-Modern-Variant)_", R"_(MIT License Modern Variant)_") +SPDX_LICENSE(R"_(MIT-advertising)_", R"_(Enlightenment License (e16))_") +SPDX_LICENSE(R"_(MIT-enna)_", R"_(enna License)_") +SPDX_LICENSE(R"_(MIT-feh)_", R"_(feh License)_") +SPDX_LICENSE(R"_(MIT-open-group)_", R"_(MIT Open Group variant)_") +SPDX_LICENSE(R"_(MITNFA)_", R"_(MIT +no-false-attribs license)_") +SPDX_LICENSE(R"_(MPL-1.0)_", R"_(Mozilla Public License 1.0)_") +SPDX_LICENSE(R"_(MPL-1.1)_", R"_(Mozilla Public License 1.1)_") +SPDX_LICENSE(R"_(MPL-2.0)_", R"_(Mozilla Public License 2.0)_") +SPDX_LICENSE(R"_(MPL-2.0-no-copyleft-exception)_", R"_(Mozilla Public License 2.0 (no copyleft exception))_") +SPDX_LICENSE(R"_(MS-PL)_", R"_(Microsoft Public License)_") +SPDX_LICENSE(R"_(MS-RL)_", R"_(Microsoft Reciprocal License)_") +SPDX_LICENSE(R"_(MTLL)_", R"_(Matrix Template Library License)_") +SPDX_LICENSE(R"_(MakeIndex)_", R"_(MakeIndex License)_") +SPDX_LICENSE(R"_(MirOS)_", R"_(The MirOS Licence)_") +SPDX_LICENSE(R"_(Motosoto)_", R"_(Motosoto License)_") +SPDX_LICENSE(R"_(MulanPSL-1.0)_", R"_(Mulan Permissive Software License, Version 1)_") +SPDX_LICENSE(R"_(MulanPSL-2.0)_", R"_(Mulan Permissive Software License, Version 2)_") +SPDX_LICENSE(R"_(Multics)_", R"_(Multics License)_") +SPDX_LICENSE(R"_(Mup)_", R"_(Mup License)_") +SPDX_LICENSE(R"_(NAIST-2003)_", R"_(Nara Institute of Science and Technology License (2003))_") +SPDX_LICENSE(R"_(NASA-1.3)_", R"_(NASA Open Source Agreement 1.3)_") +SPDX_LICENSE(R"_(NBPL-1.0)_", R"_(Net Boolean Public License v1)_") +SPDX_LICENSE(R"_(NCGL-UK-2.0)_", R"_(Non-Commercial Government Licence)_") +SPDX_LICENSE(R"_(NCSA)_", R"_(University of Illinois/NCSA Open Source License)_") +SPDX_LICENSE(R"_(NGPL)_", R"_(Nethack General Public License)_") +SPDX_LICENSE(R"_(NIST-PD)_", R"_(NIST Public Domain Notice)_") +SPDX_LICENSE(R"_(NIST-PD-fallback)_", R"_(NIST Public Domain Notice with license fallback)_") +SPDX_LICENSE(R"_(NLOD-1.0)_", R"_(Norwegian Licence for Open Government Data (NLOD) 1.0)_") +SPDX_LICENSE(R"_(NLOD-2.0)_", R"_(Norwegian Licence for Open Government Data (NLOD) 2.0)_") +SPDX_LICENSE(R"_(NLPL)_", R"_(No Limit Public License)_") +SPDX_LICENSE(R"_(NOSL)_", R"_(Netizen Open Source License)_") +SPDX_LICENSE(R"_(NPL-1.0)_", R"_(Netscape Public License v1.0)_") +SPDX_LICENSE(R"_(NPL-1.1)_", R"_(Netscape Public License v1.1)_") +SPDX_LICENSE(R"_(NPOSL-3.0)_", R"_(Non-Profit Open Software License 3.0)_") +SPDX_LICENSE(R"_(NRL)_", R"_(NRL License)_") +SPDX_LICENSE(R"_(NTP)_", R"_(NTP License)_") +SPDX_LICENSE(R"_(NTP-0)_", R"_(NTP No Attribution)_") +SPDX_LICENSE(R"_(Naumen)_", R"_(Naumen Public License)_") +SPDX_LICENSE(R"_(Net-SNMP)_", R"_(Net-SNMP License)_") +SPDX_LICENSE(R"_(NetCDF)_", R"_(NetCDF license)_") +SPDX_LICENSE(R"_(Newsletr)_", R"_(Newsletr License)_") +SPDX_LICENSE(R"_(Nokia)_", R"_(Nokia Open Source License)_") +SPDX_LICENSE(R"_(Noweb)_", R"_(Noweb License)_") +SPDX_LICENSE(R"_(Nunit)_", R"_(Nunit License)_") +SPDX_LICENSE(R"_(O-UDA-1.0)_", R"_(Open Use of Data Agreement v1.0)_") +SPDX_LICENSE(R"_(OCCT-PL)_", R"_(Open CASCADE Technology Public License)_") +SPDX_LICENSE(R"_(OCLC-2.0)_", R"_(OCLC Research Public License 2.0)_") +SPDX_LICENSE(R"_(ODC-By-1.0)_", R"_(Open Data Commons Attribution License v1.0)_") +SPDX_LICENSE(R"_(ODbL-1.0)_", R"_(Open Data Commons Open Database License v1.0)_") +SPDX_LICENSE(R"_(OFL-1.0)_", R"_(SIL Open Font License 1.0)_") +SPDX_LICENSE(R"_(OFL-1.0-RFN)_", R"_(SIL Open Font License 1.0 with Reserved Font Name)_") +SPDX_LICENSE(R"_(OFL-1.0-no-RFN)_", R"_(SIL Open Font License 1.0 with no Reserved Font Name)_") +SPDX_LICENSE(R"_(OFL-1.1)_", R"_(SIL Open Font License 1.1)_") +SPDX_LICENSE(R"_(OFL-1.1-RFN)_", R"_(SIL Open Font License 1.1 with Reserved Font Name)_") +SPDX_LICENSE(R"_(OFL-1.1-no-RFN)_", R"_(SIL Open Font License 1.1 with no Reserved Font Name)_") +SPDX_LICENSE(R"_(OGC-1.0)_", R"_(OGC Software License, Version 1.0)_") +SPDX_LICENSE(R"_(OGDL-Taiwan-1.0)_", R"_(Taiwan Open Government Data License, version 1.0)_") +SPDX_LICENSE(R"_(OGL-Canada-2.0)_", R"_(Open Government Licence - Canada)_") +SPDX_LICENSE(R"_(OGL-UK-1.0)_", R"_(Open Government Licence v1.0)_") +SPDX_LICENSE(R"_(OGL-UK-2.0)_", R"_(Open Government Licence v2.0)_") +SPDX_LICENSE(R"_(OGL-UK-3.0)_", R"_(Open Government Licence v3.0)_") +SPDX_LICENSE(R"_(OGTSL)_", R"_(Open Group Test Suite License)_") +SPDX_LICENSE(R"_(OLDAP-1.1)_", R"_(Open LDAP Public License v1.1)_") +SPDX_LICENSE(R"_(OLDAP-1.2)_", R"_(Open LDAP Public License v1.2)_") +SPDX_LICENSE(R"_(OLDAP-1.3)_", R"_(Open LDAP Public License v1.3)_") +SPDX_LICENSE(R"_(OLDAP-1.4)_", R"_(Open LDAP Public License v1.4)_") +SPDX_LICENSE(R"_(OLDAP-2.0)_", R"_(Open LDAP Public License v2.0 (or possibly 2.0A and 2.0B))_") +SPDX_LICENSE(R"_(OLDAP-2.0.1)_", R"_(Open LDAP Public License v2.0.1)_") +SPDX_LICENSE(R"_(OLDAP-2.1)_", R"_(Open LDAP Public License v2.1)_") +SPDX_LICENSE(R"_(OLDAP-2.2)_", R"_(Open LDAP Public License v2.2)_") +SPDX_LICENSE(R"_(OLDAP-2.2.1)_", R"_(Open LDAP Public License v2.2.1)_") +SPDX_LICENSE(R"_(OLDAP-2.2.2)_", R"_(Open LDAP Public License 2.2.2)_") +SPDX_LICENSE(R"_(OLDAP-2.3)_", R"_(Open LDAP Public License v2.3)_") +SPDX_LICENSE(R"_(OLDAP-2.4)_", R"_(Open LDAP Public License v2.4)_") +SPDX_LICENSE(R"_(OLDAP-2.5)_", R"_(Open LDAP Public License v2.5)_") +SPDX_LICENSE(R"_(OLDAP-2.6)_", R"_(Open LDAP Public License v2.6)_") +SPDX_LICENSE(R"_(OLDAP-2.7)_", R"_(Open LDAP Public License v2.7)_") +SPDX_LICENSE(R"_(OLDAP-2.8)_", R"_(Open LDAP Public License v2.8)_") +SPDX_LICENSE(R"_(OML)_", R"_(Open Market License)_") +SPDX_LICENSE(R"_(OPL-1.0)_", R"_(Open Public License v1.0)_") +SPDX_LICENSE(R"_(OPUBL-1.0)_", R"_(Open Publication License v1.0)_") +SPDX_LICENSE(R"_(OSET-PL-2.1)_", R"_(OSET Public License version 2.1)_") +SPDX_LICENSE(R"_(OSL-1.0)_", R"_(Open Software License 1.0)_") +SPDX_LICENSE(R"_(OSL-1.1)_", R"_(Open Software License 1.1)_") +SPDX_LICENSE(R"_(OSL-2.0)_", R"_(Open Software License 2.0)_") +SPDX_LICENSE(R"_(OSL-2.1)_", R"_(Open Software License 2.1)_") +SPDX_LICENSE(R"_(OSL-3.0)_", R"_(Open Software License 3.0)_") +SPDX_LICENSE(R"_(OpenSSL)_", R"_(OpenSSL License)_") +SPDX_LICENSE(R"_(PDDL-1.0)_", R"_(Open Data Commons Public Domain Dedication & License 1.0)_") +SPDX_LICENSE(R"_(PHP-3.0)_", R"_(PHP License v3.0)_") +SPDX_LICENSE(R"_(PHP-3.01)_", R"_(PHP License v3.01)_") +SPDX_LICENSE(R"_(PSF-2.0)_", R"_(Python Software Foundation License 2.0)_") +SPDX_LICENSE(R"_(Parity-6.0.0)_", R"_(The Parity Public License 6.0.0)_") +SPDX_LICENSE(R"_(Parity-7.0.0)_", R"_(The Parity Public License 7.0.0)_") +SPDX_LICENSE(R"_(Plexus)_", R"_(Plexus Classworlds License)_") +SPDX_LICENSE(R"_(PolyForm-Noncommercial-1.0.0)_", R"_(PolyForm Noncommercial License 1.0.0)_") +SPDX_LICENSE(R"_(PolyForm-Small-Business-1.0.0)_", R"_(PolyForm Small Business License 1.0.0)_") +SPDX_LICENSE(R"_(PostgreSQL)_", R"_(PostgreSQL License)_") +SPDX_LICENSE(R"_(Python-2.0)_", R"_(Python License 2.0)_") +SPDX_LICENSE(R"_(QPL-1.0)_", R"_(Q Public License 1.0)_") +SPDX_LICENSE(R"_(Qhull)_", R"_(Qhull License)_") +SPDX_LICENSE(R"_(RHeCos-1.1)_", R"_(Red Hat eCos Public License v1.1)_") +SPDX_LICENSE(R"_(RPL-1.1)_", R"_(Reciprocal Public License 1.1)_") +SPDX_LICENSE(R"_(RPL-1.5)_", R"_(Reciprocal Public License 1.5)_") +SPDX_LICENSE(R"_(RPSL-1.0)_", R"_(RealNetworks Public Source License v1.0)_") +SPDX_LICENSE(R"_(RSA-MD)_", R"_(RSA Message-Digest License)_") +SPDX_LICENSE(R"_(RSCPL)_", R"_(Ricoh Source Code Public License)_") +SPDX_LICENSE(R"_(Rdisc)_", R"_(Rdisc License)_") +SPDX_LICENSE(R"_(Ruby)_", R"_(Ruby License)_") +SPDX_LICENSE(R"_(SAX-PD)_", R"_(Sax Public Domain Notice)_") +SPDX_LICENSE(R"_(SCEA)_", R"_(SCEA Shared Source License)_") +SPDX_LICENSE(R"_(SGI-B-1.0)_", R"_(SGI Free Software License B v1.0)_") +SPDX_LICENSE(R"_(SGI-B-1.1)_", R"_(SGI Free Software License B v1.1)_") +SPDX_LICENSE(R"_(SGI-B-2.0)_", R"_(SGI Free Software License B v2.0)_") +SPDX_LICENSE(R"_(SHL-0.5)_", R"_(Solderpad Hardware License v0.5)_") +SPDX_LICENSE(R"_(SHL-0.51)_", R"_(Solderpad Hardware License, Version 0.51)_") +SPDX_LICENSE(R"_(SISSL)_", R"_(Sun Industry Standards Source License v1.1)_") +SPDX_LICENSE(R"_(SISSL-1.2)_", R"_(Sun Industry Standards Source License v1.2)_") +SPDX_LICENSE(R"_(SMLNJ)_", R"_(Standard ML of New Jersey License)_") +SPDX_LICENSE(R"_(SMPPL)_", R"_(Secure Messaging Protocol Public License)_") +SPDX_LICENSE(R"_(SNIA)_", R"_(SNIA Public License 1.1)_") +SPDX_LICENSE(R"_(SPL-1.0)_", R"_(Sun Public License v1.0)_") +SPDX_LICENSE(R"_(SSH-OpenSSH)_", R"_(SSH OpenSSH license)_") +SPDX_LICENSE(R"_(SSH-short)_", R"_(SSH short notice)_") +SPDX_LICENSE(R"_(SSPL-1.0)_", R"_(Server Side Public License, v 1)_") +SPDX_LICENSE(R"_(SWL)_", R"_(Scheme Widget Library (SWL) Software License Agreement)_") +SPDX_LICENSE(R"_(Saxpath)_", R"_(Saxpath License)_") +SPDX_LICENSE(R"_(SchemeReport)_", R"_(Scheme Language Report License)_") +SPDX_LICENSE(R"_(Sendmail)_", R"_(Sendmail License)_") +SPDX_LICENSE(R"_(Sendmail-8.23)_", R"_(Sendmail License 8.23)_") +SPDX_LICENSE(R"_(SimPL-2.0)_", R"_(Simple Public License 2.0)_") +SPDX_LICENSE(R"_(Sleepycat)_", R"_(Sleepycat License)_") +SPDX_LICENSE(R"_(Spencer-86)_", R"_(Spencer License 86)_") +SPDX_LICENSE(R"_(Spencer-94)_", R"_(Spencer License 94)_") +SPDX_LICENSE(R"_(Spencer-99)_", R"_(Spencer License 99)_") +SPDX_LICENSE(R"_(StandardML-NJ)_", R"_(Standard ML of New Jersey License)_") +SPDX_LICENSE(R"_(SugarCRM-1.1.3)_", R"_(SugarCRM Public License v1.1.3)_") +SPDX_LICENSE(R"_(TAPR-OHL-1.0)_", R"_(TAPR Open Hardware License v1.0)_") +SPDX_LICENSE(R"_(TCL)_", R"_(TCL/TK License)_") +SPDX_LICENSE(R"_(TCP-wrappers)_", R"_(TCP Wrappers License)_") +SPDX_LICENSE(R"_(TMate)_", R"_(TMate Open Source License)_") +SPDX_LICENSE(R"_(TORQUE-1.1)_", R"_(TORQUE v2.5+ Software License v1.1)_") +SPDX_LICENSE(R"_(TOSL)_", R"_(Trusster Open Source License)_") +SPDX_LICENSE(R"_(TU-Berlin-1.0)_", R"_(Technische Universitaet Berlin License 1.0)_") +SPDX_LICENSE(R"_(TU-Berlin-2.0)_", R"_(Technische Universitaet Berlin License 2.0)_") +SPDX_LICENSE(R"_(UCL-1.0)_", R"_(Upstream Compatibility License v1.0)_") +SPDX_LICENSE(R"_(UPL-1.0)_", R"_(Universal Permissive License v1.0)_") +SPDX_LICENSE(R"_(Unicode-DFS-2015)_", R"_(Unicode License Agreement - Data Files and Software (2015))_") +SPDX_LICENSE(R"_(Unicode-DFS-2016)_", R"_(Unicode License Agreement - Data Files and Software (2016))_") +SPDX_LICENSE(R"_(Unicode-TOU)_", R"_(Unicode Terms of Use)_") +SPDX_LICENSE(R"_(Unlicense)_", R"_(The Unlicense)_") +SPDX_LICENSE(R"_(VOSTROM)_", R"_(VOSTROM Public License for Open Source)_") +SPDX_LICENSE(R"_(VSL-1.0)_", R"_(Vovida Software License v1.0)_") +SPDX_LICENSE(R"_(Vim)_", R"_(Vim License)_") +SPDX_LICENSE(R"_(W3C)_", R"_(W3C Software Notice and License (2002-12-31))_") +SPDX_LICENSE(R"_(W3C-19980720)_", R"_(W3C Software Notice and License (1998-07-20))_") +SPDX_LICENSE(R"_(W3C-20150513)_", R"_(W3C Software Notice and Document License (2015-05-13))_") +SPDX_LICENSE(R"_(WTFPL)_", R"_(Do What The F*ck You Want To Public License)_") +SPDX_LICENSE(R"_(Watcom-1.0)_", R"_(Sybase Open Watcom Public License 1.0)_") +SPDX_LICENSE(R"_(Wsuipa)_", R"_(Wsuipa License)_") +SPDX_LICENSE(R"_(X11)_", R"_(X11 License)_") +SPDX_LICENSE(R"_(XFree86-1.1)_", R"_(XFree86 License 1.1)_") +SPDX_LICENSE(R"_(XSkat)_", R"_(XSkat License)_") +SPDX_LICENSE(R"_(Xerox)_", R"_(Xerox License)_") +SPDX_LICENSE(R"_(Xnet)_", R"_(X.Net License)_") +SPDX_LICENSE(R"_(YPL-1.0)_", R"_(Yahoo! Public License v1.0)_") +SPDX_LICENSE(R"_(YPL-1.1)_", R"_(Yahoo! Public License v1.1)_") +SPDX_LICENSE(R"_(ZPL-1.1)_", R"_(Zope Public License 1.1)_") +SPDX_LICENSE(R"_(ZPL-2.0)_", R"_(Zope Public License 2.0)_") +SPDX_LICENSE(R"_(ZPL-2.1)_", R"_(Zope Public License 2.1)_") +SPDX_LICENSE(R"_(Zed)_", R"_(Zed License)_") +SPDX_LICENSE(R"_(Zend-2.0)_", R"_(Zend License v2.0)_") +SPDX_LICENSE(R"_(Zimbra-1.3)_", R"_(Zimbra Public License v1.3)_") +SPDX_LICENSE(R"_(Zimbra-1.4)_", R"_(Zimbra Public License v1.4)_") +SPDX_LICENSE(R"_(Zlib)_", R"_(zlib License)_") +SPDX_LICENSE(R"_(blessing)_", R"_(SQLite Blessing)_") +SPDX_LICENSE(R"_(bzip2-1.0.5)_", R"_(bzip2 and libbzip2 License v1.0.5)_") +SPDX_LICENSE(R"_(bzip2-1.0.6)_", R"_(bzip2 and libbzip2 License v1.0.6)_") +SPDX_LICENSE(R"_(copyleft-next-0.3.0)_", R"_(copyleft-next 0.3.0)_") +SPDX_LICENSE(R"_(copyleft-next-0.3.1)_", R"_(copyleft-next 0.3.1)_") +SPDX_LICENSE(R"_(curl)_", R"_(curl License)_") +SPDX_LICENSE(R"_(diffmark)_", R"_(diffmark license)_") +SPDX_LICENSE(R"_(dvipdfm)_", R"_(dvipdfm License)_") +SPDX_LICENSE(R"_(eCos-2.0)_", R"_(eCos license version 2.0)_") +SPDX_LICENSE(R"_(eGenix)_", R"_(eGenix.com Public License 1.1.0)_") +SPDX_LICENSE(R"_(etalab-2.0)_", R"_(Etalab Open License 2.0)_") +SPDX_LICENSE(R"_(gSOAP-1.3b)_", R"_(gSOAP Public License v1.3b)_") +SPDX_LICENSE(R"_(gnuplot)_", R"_(gnuplot License)_") +SPDX_LICENSE(R"_(iMatix)_", R"_(iMatix Standard Function Library Agreement)_") +SPDX_LICENSE(R"_(libpng-2.0)_", R"_(PNG Reference Library version 2)_") +SPDX_LICENSE(R"_(libselinux-1.0)_", R"_(libselinux public domain notice)_") +SPDX_LICENSE(R"_(libtiff)_", R"_(libtiff License)_") +SPDX_LICENSE(R"_(mpich2)_", R"_(mpich2 License)_") +SPDX_LICENSE(R"_(psfrag)_", R"_(psfrag License)_") +SPDX_LICENSE(R"_(psutils)_", R"_(psutils License)_") +SPDX_LICENSE(R"_(wxWindows)_", R"_(wxWindows Library License)_") +SPDX_LICENSE(R"_(xinetd)_", R"_(xinetd License)_") +SPDX_LICENSE(R"_(xpp)_", R"_(XPP License)_") +SPDX_LICENSE(R"_(zlib-acknowledgement)_", R"_(zlib/libpng License with Acknowledgement)_") \ No newline at end of file diff --git a/src/bpt/project/spdx.test.cpp b/src/bpt/project/spdx.test.cpp new file mode 100644 index 00000000..bd81ba63 --- /dev/null +++ b/src/bpt/project/spdx.test.cpp @@ -0,0 +1,24 @@ +#include "./spdx.hpp" + +#include +#include + +auto parse_spdx(std::string_view sv) { + return REQUIRES_LEAF_NOFAIL(bpt::spdx_license_expression::parse(sv)); + // return bpt::spdx_license_expression::parse(sv); +} + +TEST_CASE("Parse a license string") { + auto expr = parse_spdx("BSL-1.0"); + + CHECK(expr.to_string() == "BSL-1.0"); + + expr = parse_spdx("BSL-1.0+"); + CHECK(expr.to_string() == "BSL-1.0+"); + + auto conjunct = parse_spdx("BSL-1.0 AND MPL-1.0"); + CHECK(conjunct.to_string() == "BSL-1.0 AND MPL-1.0"); + + conjunct = parse_spdx("(BSL-1.0 AND MPL-1.0)"); + CHECK(conjunct.to_string() == "(BSL-1.0 AND MPL-1.0)"); +} diff --git a/src/bpt/sdist/dist.cpp b/src/bpt/sdist/dist.cpp new file mode 100644 index 00000000..998116d9 --- /dev/null +++ b/src/bpt/sdist/dist.cpp @@ -0,0 +1,151 @@ +#include "./dist.hpp" + +#include "./error.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace bpt; +using namespace fansi::literals; + +namespace { + +void sdist_export_file(path_ref out_root, path_ref in_root, path_ref filepath) { + auto relpath = fs::relative(filepath, in_root); + bpt_log(debug, "Export file {}", relpath.string()); + auto dest = out_root / relpath; + fs::create_directories(fs::absolute(dest).parent_path()); + fs::copy(filepath, dest); +} + +void sdist_copy_library(path_ref out_root, + const sdist& sd, + const crs::library_info& lib, + const sdist_params& params) { + auto lib_dir = bpt::resolve_path_strong(sd.path / lib.path).value(); + auto inc_dir = source_root{lib_dir / "include"}; + auto src_dir = source_root{lib_dir / "src"}; + + auto inc_sources = inc_dir.exists() ? inc_dir.collect_sources() : std::vector{}; + auto src_sources = src_dir.exists() ? src_dir.collect_sources() : std::vector{}; + + using namespace std::views; + auto all_arr = std::array{all(inc_sources), all(src_sources)}; + + bpt_log(info, "sdist: Export library from {}", lib_dir.string()); + fs::create_directories(out_root); + for (const auto& source : all_arr | join) { + sdist_export_file(out_root, params.project_dir, source.path); + } +} + +} // namespace + +sdist bpt::create_sdist(const sdist_params& params) { + auto dest = fs::absolute(params.dest_path); + if (fs::exists(dest)) { + if (!params.force) { + throw_user_error("Destination path '{}' already exists", + dest.string()); + } + } + + auto tempdir = temporary_dir::create(); + create_sdist_in_dir(tempdir.path(), params); + if (fs::exists(dest) && params.force) { + fs::remove_all(dest); + } + fs::create_directories(fs::absolute(dest).parent_path()); + move_file(tempdir.path(), dest).value(); + bpt_log(info, "Source distribution created in {}", dest.string()); + return sdist::from_directory(dest); +} + +void bpt::create_sdist_targz(path_ref filepath, const sdist_params& params) { + if (fs::exists(filepath)) { + if (!params.force) { + throw_user_error("Destination path '{}' already exists", + filepath.string()); + } + if (fs::is_directory(filepath)) { + throw_user_error("Destination path '{}' is a directory", + filepath.string()); + } + } + + auto tempdir = temporary_dir::create(); + bpt_log(debug, "Generating source distribution in {}", tempdir.path().string()); + create_sdist_in_dir(tempdir.path(), params); + fs::create_directories(fs::absolute(filepath).parent_path()); + neo::compress_directory_targz(tempdir.path(), filepath); +} + +sdist bpt::create_sdist_in_dir(path_ref out, const sdist_params& params) { + auto in_sd = sdist::from_directory(params.project_dir); + for (const crs::library_info& lib : in_sd.pkg.libraries) { + sdist_copy_library(out, in_sd, lib, params); + } + in_sd.pkg.id.revision = params.revision; + + fs::create_directories(out); + bpt::write_file(out / "pkg.json", in_sd.pkg.to_json(2)); + return sdist::from_directory(out); +} + +sdist sdist::from_directory(path_ref where) { + BPT_E_SCOPE(e_sdist_from_directory{where}); + crs::package_info meta; + + auto pkg_json = where / "pkg.json"; + auto bpt_yaml = where / "bpt.yaml"; + const bool have_pkg_json = bpt::file_exists(pkg_json); + const bool have_bpt_yaml = bpt::file_exists(bpt_yaml); + + if (have_pkg_json) { + if (have_bpt_yaml) { + bpt_log( + warn, + "Directory has both [.cyan[{}]] and [.cyan[{}]] (The .bold.cyan[pkg`.json] file will be preferred)"_styled, + pkg_json.string(), + bpt_yaml.string()); + } + BPT_E_SCOPE(crs::e_pkg_json_path{pkg_json}); + auto data = bpt::parse_json5_file(pkg_json); + meta = crs::package_info::from_json_data(data); + } else { + auto proj = project::open_directory(where); + if (!proj.manifest.has_value()) { + BOOST_LEAF_THROW_EXCEPTION( + make_user_error( + "No pkg.json nor project manifest in the project directory"), + e_missing_pkg_json{pkg_json}, + e_missing_project_yaml{where / "bpt.yaml"}, + BPT_ERR_REF("invalid-pkg-filesystem")); + } + meta = proj.manifest->as_crs_package_meta(); + } + return sdist{meta, where}; +} diff --git a/src/bpt/sdist/dist.hpp b/src/bpt/sdist/dist.hpp new file mode 100644 index 00000000..6fd0c9db --- /dev/null +++ b/src/bpt/sdist/dist.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +namespace bpt { + +struct sdist_params { + fs::path project_dir; + fs::path dest_path; + bool force = false; + int revision = 1; +}; + +struct sdist { + crs::package_info pkg; + fs::path path; + + sdist(crs::package_info pkg_, path_ref path_) noexcept + : pkg{pkg_} + , path{path_} {} + + static sdist from_directory(path_ref p); +}; + +sdist create_sdist(const sdist_params&); +sdist create_sdist_in_dir(path_ref, const sdist_params&); +void create_sdist_targz(path_ref, const sdist_params&); + +} // namespace bpt diff --git a/src/bpt/sdist/error.hpp b/src/bpt/sdist/error.hpp new file mode 100644 index 00000000..4be85e54 --- /dev/null +++ b/src/bpt/sdist/error.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include + +namespace bpt { + +struct e_sdist_from_directory { + std::filesystem::path value; +}; + +struct e_missing_pkg_json { + std::filesystem::path value; +}; + +struct e_missing_project_yaml { + std::filesystem::path value; +}; + +} // namespace bpt diff --git a/src/dds/sdist/file.cpp b/src/bpt/sdist/file.cpp similarity index 63% rename from src/dds/sdist/file.cpp rename to src/bpt/sdist/file.cpp index 2e43ffc1..48de5063 100644 --- a/src/dds/sdist/file.cpp +++ b/src/bpt/sdist/file.cpp @@ -1,15 +1,15 @@ #include "./file.hpp" -#include +#include #include #include #include #include -using namespace dds; +using namespace bpt; -std::optional dds::infer_source_kind(path_ref p) noexcept { +std::optional bpt::infer_source_kind(path_ref p) noexcept { static std::vector header_exts = { ".H", ".H++", @@ -18,11 +18,12 @@ std::optional dds::infer_source_kind(path_ref p) noexcept { ".hh", ".hpp", ".hxx", + }; + static std::vector header_impl_exts = { ".inc", ".inl", ".ipp", }; - assert(std::is_sorted(header_exts.begin(), header_exts.end())); static std::vector source_exts = { ".C", ".c", @@ -32,22 +33,24 @@ std::optional dds::infer_source_kind(path_ref p) noexcept { ".cxx", }; assert(std::is_sorted(header_exts.begin(), header_exts.end())); + assert(std::is_sorted(header_impl_exts.begin(), header_impl_exts.end())); assert(std::is_sorted(source_exts.begin(), source_exts.end())); auto leaf = p.filename(); - auto ext_found - = std::lower_bound(header_exts.begin(), header_exts.end(), p.extension(), std::less<>()); - if (ext_found != header_exts.end() && *ext_found == p.extension()) { - auto stem = p.stem(); - if (stem.extension() == ".config") { - return source_kind::header_template; - } + const auto is_in = [](const auto& exts, const auto& ext) { + auto ext_found = std::lower_bound(exts.begin(), exts.end(), ext, std::less<>()); + return ext_found != exts.end() && *ext_found == ext; + }; + + if (is_in(header_exts, p.extension())) { return source_kind::header; } - ext_found - = std::lower_bound(source_exts.begin(), source_exts.end(), p.extension(), std::less<>()); - if (ext_found == source_exts.end() || *ext_found != p.extension()) { + if (is_in(header_impl_exts, p.extension())) { + return source_kind::header_impl; + } + + if (!is_in(source_exts, p.extension())) { return std::nullopt; } diff --git a/src/dds/sdist/file.hpp b/src/bpt/sdist/file.hpp similarity index 66% rename from src/dds/sdist/file.hpp rename to src/bpt/sdist/file.hpp index b5e396e2..c85db7f9 100644 --- a/src/dds/sdist/file.hpp +++ b/src/bpt/sdist/file.hpp @@ -1,20 +1,26 @@ #pragma once -#include +#include #include #include -namespace dds { +namespace bpt { enum class source_kind { + // Pure header files, e.g. .h header, - header_template, + // "Header" implementation files which are #included in header files. e.g. .inl + header_impl, source, test, app, }; +constexpr bool is_header(source_kind kind) { + return kind == source_kind::header || kind == source_kind::header_impl; +} + std::optional infer_source_kind(path_ref) noexcept; struct source_file { @@ -36,6 +42,4 @@ struct source_file { fs::path relative_path() const noexcept { return fs::relative(path, basis_path); } }; -using source_list = std::vector; - -} // namespace dds \ No newline at end of file +} // namespace bpt diff --git a/src/dds/sdist/file.test.cpp b/src/bpt/sdist/file.test.cpp similarity index 67% rename from src/dds/sdist/file.test.cpp rename to src/bpt/sdist/file.test.cpp index 853ee31f..d31a0e4f 100644 --- a/src/dds/sdist/file.test.cpp +++ b/src/bpt/sdist/file.test.cpp @@ -1,16 +1,15 @@ -#include +#include #include -using dds::source_kind; +using bpt::source_kind; TEST_CASE("Infer source kind") { - using dds::infer_source_kind; + using bpt::infer_source_kind; auto k = infer_source_kind("foo.h"); CHECK(k == source_kind::header); CHECK(infer_source_kind("foo.hpp") == source_kind::header); CHECK_FALSE(infer_source_kind("foo.txt")); // Not a source file extension CHECK(infer_source_kind("foo.hh") == source_kind::header); - CHECK(infer_source_kind("foo.config.hpp") == source_kind::header_template); -} \ No newline at end of file +} diff --git a/src/bpt/sdist/root.cpp b/src/bpt/sdist/root.cpp new file mode 100644 index 00000000..6288ea90 --- /dev/null +++ b/src/bpt/sdist/root.cpp @@ -0,0 +1,38 @@ +#include "./root.hpp" + +#include + +#include +#include +#include + +#include + +using namespace bpt; + +namespace { + +struct collector_state { + fs::path base_path; + fs::recursive_directory_iterator dir_iter; +}; + +} // namespace + +bpt::collected_sources bpt::collect_sources(path_ref dirpath) { + using namespace ranges::views; + auto state + = neo::copy_shared(collector_state{dirpath, fs::recursive_directory_iterator{dirpath}}); + return state->dir_iter // + | filter(BPT_TL(_1.is_regular_file())) // + | transform([state] BPT_CTL(source_file::from_path(_1, state->base_path))) // + | filter(BPT_TL(_1.has_value())) // + | transform([state] BPT_CTL(source_file{std::move(*_1)})) // + ; +} + +std::vector source_root::collect_sources() const { + using namespace ranges::views; + // Collect all source files from the directory + return bpt::collect_sources(path) | neo::to_vector; +} diff --git a/src/dds/sdist/root.hpp b/src/bpt/sdist/root.hpp similarity index 57% rename from src/dds/sdist/root.hpp rename to src/bpt/sdist/root.hpp index ee139eb3..f682f63d 100644 --- a/src/dds/sdist/root.hpp +++ b/src/bpt/sdist/root.hpp @@ -1,11 +1,21 @@ #pragma once -#include -#include +#include +#include +#include + +#include #include -namespace dds { +namespace bpt { + +struct collected_sources : neo::any_range, + std::ranges::view_interface { + using any_range::any_range; +}; + +collected_sources collect_sources(path_ref dirpath); /** * A `source_root` is a simple wrapper type that provides type safety and utilities to @@ -26,4 +36,4 @@ struct source_root { bool exists() const noexcept { return fs::exists(path); } }; -} // namespace dds +} // namespace bpt diff --git a/src/bpt/solve/solve.cpp b/src/bpt/solve/solve.cpp new file mode 100644 index 00000000..ad7d5220 --- /dev/null +++ b/src/bpt/solve/solve.cpp @@ -0,0 +1,379 @@ +#include "./solve.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace bpt; +using namespace bpt::crs; + +namespace sr = std::ranges; +namespace stdv = std::views; + +namespace { + +/** + * @brief Given a version range set that only covers a single version, return + * that single version object + */ +semver::version sole_version(const crs::version_range_set& versions) { + neo_assert(invariant, + versions.num_intervals() == 1, + "sole_version() must only present a single version", + versions.num_intervals()); + return (*versions.iter_intervals().begin()).low; +} + +using name_vec = std::vector; + +struct requirement { + bpt::name name; + crs::version_range_set versions; + name_vec uses; + std::optional pkg_version; + + explicit requirement(bpt::name name, + crs::version_range_set vrs, + name_vec u, + std::optional pver) + : name{std::move(name)} + , versions{std::move(vrs)} + , uses{std::move(u)} + , pkg_version{pver} { + sr::sort(uses); + } + + static requirement from_crs_dep(const crs::dependency& dep) noexcept { + return requirement{dep.name, dep.acceptable_versions, dep.uses, std::nullopt}; + } + + auto& key() const noexcept { return name; } + + bool implied_by(const requirement& other) const noexcept { + if (!versions.contains(other.versions)) { + return false; + } + if (sr::includes(other.uses, uses)) { + return true; + } + return false; + } + + bool excludes(const requirement& other) const noexcept { + return versions.disjoint(other.versions); + } + + name_vec union_usages(name_vec const& l, name_vec const& r) const noexcept { + name_vec uses_union; + sr::set_union(l, r, std::back_inserter(uses_union)); + return uses_union; + } + + name_vec intersect_usages(name_vec const& l, name_vec const& r) const noexcept { + name_vec uses_intersection; + sr::set_intersection(l, r, std::back_inserter(uses_intersection)); + return uses_intersection; + } + + std::optional intersection(const requirement& other) const noexcept { + auto range = versions.intersection(other.versions); + if (range.empty()) { + return std::nullopt; + } + return requirement{name, std::move(range), union_usages(uses, other.uses), std::nullopt}; + } + + std::optional union_(const requirement& other) const noexcept { + auto range = versions.union_(other.versions); + auto uses_isect = intersect_usages(uses, other.uses); + if (range.empty()) { + return std::nullopt; + } + return requirement{name, std::move(range), std::move(uses_isect), std::nullopt}; + } + + std::optional difference(const requirement& other) const noexcept { + auto range = versions.difference(other.versions); + if (range.empty() and (uses.empty() or uses == other.uses)) { + return std::nullopt; + } + return requirement{name, std::move(range), union_usages(uses, other.uses), std::nullopt}; + } + + std::string decl_to_string() const noexcept { + return crs::dependency{name, versions, uses}.decl_to_string(); + } + + friend std::ostream& operator<<(std::ostream& out, const requirement& self) noexcept { + out << self.decl_to_string(); + return out; + } + + friend void do_repr(auto out, const requirement* self) noexcept { + out.type("bpt-requirement"); + if (self) { + out.value(self->decl_to_string()); + } + } +}; + +struct metadata_provider { + crs::cache_db const& cache_db; + + mutable std::map, std::less<>> + pkgs_by_name{}; + + const std::vector& + packages_for_name(std::string_view name) const { + auto found = pkgs_by_name.find(name); + if (found == pkgs_by_name.end()) { + found + = pkgs_by_name + .emplace(std::string(name), + cache_db.for_package(bpt::name{std::string(name)}) | neo::to_vector) + .first; + sr::sort(found->second, + std::less<>{}, + BPT_TL(std::make_tuple(_1.pkg.id.version, -_1.pkg.id.revision))); + } + return found->second; + } + + std::optional best_candidate(const requirement& req) const { + bpt::cancellation_point(); + auto& pkgs = packages_for_name(req.name.str); + bpt_log(debug, "Find best candidate of {}", req.decl_to_string()); + auto cand = sr::find_if(pkgs, [&](auto&& entry) { + if (!req.versions.contains(entry.pkg.id.version)) { + return false; + } + bool has_all_libraries = sr::all_of(req.uses, [&](auto&& uses_name) { + return sr::any_of(entry.pkg.libraries, BPT_TL(uses_name == _1.name)); + }); + if (!has_all_libraries) { + bpt_log(debug, + " Near match: {} (missing one or more required libraries)", + entry.pkg.id.to_string()); + } + return has_all_libraries; + }); + + if (cand == pkgs.cend()) { + bpt_log(debug, " (No candidate)"); + return std::nullopt; + } + + bpt_log(debug, " Best candidate: {}", cand->pkg.id.to_string()); + + return requirement{cand->pkg.id.name, + {cand->pkg.id.version, cand->pkg.id.version.next_after()}, + req.uses, + cand->pkg.id.revision}; + } + + /** + * @brief Look up the requirements of the given package + * + * @param req A requirement of a single version of a package, plus the libraries within that + * package that are required + * @return std::vector The packages that are required + */ + std::vector requirements_of(const requirement& req) const { + bpt::cancellation_point(); + bpt_log(trace, "Lookup dependencies of {}", req.decl_to_string()); + const auto& version = sole_version(req.versions); + auto metas = cache_db.for_package(req.name, version); + auto it = sr::begin(metas); + neo_assert(invariant, + it != sr::end(metas), + "Unexpected empty metadata for requirements of package {}@{}", + req.name.str, + version.to_string()); + auto pkg = it->pkg; + + std::set uses; + extend(uses, req.uses); + std::set more_uses; + while (1) { + more_uses = uses; + for (auto& used : uses) { + auto lib_it = sr::find(pkg.libraries, used, &crs::library_info::name); + neo_assert(invariant, + lib_it != sr::end(pkg.libraries), + "Invalid 'using' on non-existent requirement library", + used, + pkg); + extend(more_uses, lib_it->intra_using); + } + if (sr::includes(uses, more_uses)) { + break; + } + uses = more_uses; + } + + auto reqs = pkg.libraries // + | stdv::filter([&](auto&& lib) { return uses.contains(lib.name); }) // + | stdv::transform(BPT_TL(_1.dependencies)) // + | stdv::join // + | stdv::transform(BPT_TL(requirement::from_crs_dep(_1))) // + | neo::to_vector; + for (auto&& r : reqs) { + bpt_log(trace, " Requires: {}", r.decl_to_string()); + } + if (reqs.empty()) { + bpt_log(trace, " (No dependencies)"); + } + return reqs; + } + + void debug(std::string_view sv) const noexcept { bpt_log(trace, "pubgrub: {}", sv); } +}; + +using solve_failure_exception = pubgrub::solve_failure_type_t; + +struct fail_explainer { + std::stringstream part; + std::stringstream strm; + bool at_head = true; + + void put(pubgrub::explain::no_solution) { part << "dependencies cannot be satisfied"; } + + void put(pubgrub::explain::dependency dep) { + fmt::print(part, "{} requires {}", dep.dependent, dep.dependency); + } + + void put(pubgrub::explain::unavailable un) { + fmt::print(part, "{} is not available", un.requirement); + } + + void put(pubgrub::explain::conflict cf) { + fmt::print(part, "{} conflicts with {}", cf.a, cf.b); + } + + void put(pubgrub::explain::needed req) { + fmt::print(part, "{} is required", req.requirement); + } + + void put(pubgrub::explain::disallowed dis) { + fmt::print(part, "{} cannot be used", dis.requirement); + } + + void put(pubgrub::explain::compromise cmpr) { + fmt::print(part, "{} and {} agree on {}", cmpr.left, cmpr.right, cmpr.result); + } + + template + void operator()(pubgrub::explain::premise pr) { + part.str(""); + put(pr.value); + fmt::print(strm, "{} {},\n", at_head ? "┌─ Given that" : "│ and that", part.str()); + at_head = false; + } + + template + void operator()(pubgrub::explain::conclusion cncl) { + at_head = true; + part.str(""); + put(cncl.value); + fmt::print(strm, "╘═ then {}.\n", part.str()); + } + + void operator()(pubgrub::explain::separator) { strm << "\n"; } +}; + +bpt::e_dependency_solve_failure_explanation +generate_failure_explanation(const solve_failure_exception& exc) { + fail_explainer explain; + pubgrub::generate_explaination(exc, explain); + return e_dependency_solve_failure_explanation{explain.strm.str()}; +} + +void try_load_nonesuch_packages(boost::leaf::error_id error, + crs::cache_db const& cache, + const std::vector& reqs) { + // Find packages and libraries that aren't at all available + error.load([&](std::vector& missing) { + for (auto& req : reqs) { + auto cands = cache.for_package(req.name); + auto it = cands.begin(); + if (it == cands.end()) { + // This requirement has no candidates + auto all = cache.all_enabled(); + auto all_names = all + | stdv::transform([](auto entry) { return entry.pkg.id.name.str; }) + | neo::to_vector; + missing.emplace_back(req.name.str, did_you_mean(req.name.str, all_names)); + continue; + } + error.load([&](std::vector& missing_libs) { + auto want_libs = req.uses; + sr::sort(want_libs); + std::set all_lib_names; + for (; it != cands.end() and not want_libs.empty(); ++it) { + auto cand_has_libs = it->pkg.libraries + | stdv::transform(&crs::library_info::name) | neo::to_vector; + sr::sort(cand_has_libs); + std::vector missing_libs; + sr::set_difference(want_libs, cand_has_libs, std::back_inserter(missing_libs)); + want_libs = std::move(missing_libs); + extend(all_lib_names, cand_has_libs | stdv::transform(&bpt::name::str)); + } + if (not want_libs.empty()) { + extend(missing_libs, + want_libs | stdv::transform([&](auto&& libname) { + return e_nonesuch_using_library{ + req.name, + e_nonesuch{libname.str, + did_you_mean(libname.str, all_lib_names)}}; + })); + } + }); + } + }); +} + +} // namespace + +std::vector bpt::solve(crs::cache_db const& cache, + neo::any_input_range deps_) { + metadata_provider provider{cache}; + auto deps = deps_ | stdv::transform(BPT_TL(requirement::from_crs_dep(_1))) | neo::to_vector; + auto sln = bpt_leaf_try { return pubgrub::solve(deps, provider); } + bpt_leaf_catch(catch_ exc)->noreturn_t { + auto error = boost::leaf::new_error(); + try_load_nonesuch_packages(error, cache, deps); + BOOST_LEAF_THROW_EXCEPTION(error, + bpt::e_dependency_solve_failure{}, + BPT_E_ARG(generate_failure_explanation(exc.matched)), + BPT_ERR_REF("dep-res-failure")); + }; + return sln + | stdv::transform(BPT_TL(crs::pkg_id{ + .name = _1.name, + .version = sole_version(_1.versions), + .revision = _1.pkg_version.value(), + })) + | neo::to_vector; +} diff --git a/src/bpt/solve/solve.hpp b/src/bpt/solve/solve.hpp new file mode 100644 index 00000000..fc898d20 --- /dev/null +++ b/src/bpt/solve/solve.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +#include +#include + +#include + +namespace bpt { + +namespace crs { + +class cache_db; +struct dependency; + +} // namespace crs + +struct e_usage_no_such_lib {}; + +struct e_dependency_solve_failure {}; +struct e_dependency_solve_failure_explanation { + std::string value; +}; + +struct e_nonesuch_package : e_nonesuch { + using e_nonesuch::e_nonesuch; +}; + +struct e_nonesuch_using_library { + bpt::name pkg_name; + e_nonesuch lib; +}; + +std::vector solve(crs::cache_db const&, neo::any_input_range); + +} // namespace bpt diff --git a/src/dds/temp.hpp b/src/bpt/temp.hpp similarity index 64% rename from src/dds/temp.hpp rename to src/bpt/temp.hpp index 3aec6ab0..f08b5f50 100644 --- a/src/dds/temp.hpp +++ b/src/bpt/temp.hpp @@ -1,10 +1,10 @@ #pragma once -#include +#include #include -namespace dds { +namespace bpt { class temporary_dir { struct impl { @@ -16,9 +16,7 @@ class temporary_dir { ~impl() { std::error_code ec; - if (fs::exists(path, ec)) { - fs::remove_all(path, ec); - } + fs::remove_all(path, ec); } }; @@ -28,9 +26,10 @@ class temporary_dir { : _ptr(p) {} public: - static temporary_dir create(); + static temporary_dir create_in(path_ref parent); + static temporary_dir create() { return create_in(fs::temp_directory_path()); } path_ref path() const noexcept { return _ptr->path; } }; -} // namespace dds \ No newline at end of file +} // namespace bpt \ No newline at end of file diff --git a/src/dds/temp.nix.cpp b/src/bpt/temp.nix.cpp similarity index 54% rename from src/dds/temp.nix.cpp rename to src/bpt/temp.nix.cpp index 9b6a5df8..15d7fb18 100644 --- a/src/dds/temp.nix.cpp +++ b/src/bpt/temp.nix.cpp @@ -1,14 +1,18 @@ -#ifndef _WIN32 #include "./temp.hpp" -using namespace dds; +#ifndef _WIN32 + +#ifdef __APPLE__ +#include +#endif -temporary_dir temporary_dir::create() { - auto base = fs::temp_directory_path(); - auto file = (base / "dds-tmp-XXXXXX").string(); +using namespace bpt; - const char* tempdir_path = ::mktemp(file.data()); +temporary_dir temporary_dir::create_in(path_ref base) { + fs::create_directories(base); + auto file = (base / "bpt-tmp-XXXXXX").string(); + const char* tempdir_path = ::mkdtemp(file.data()); if (tempdir_path == nullptr) { throw std::system_error(std::error_code(errno, std::system_category()), "Failed to create a temporary directory"); @@ -16,4 +20,5 @@ temporary_dir temporary_dir::create() { auto path = fs::path(tempdir_path); return std::make_shared(std::move(path)); } -#endif \ No newline at end of file + +#endif diff --git a/src/dds/temp.win.cpp b/src/bpt/temp.win.cpp similarity index 72% rename from src/dds/temp.win.cpp rename to src/bpt/temp.win.cpp index 4a3eee7f..cfd150d8 100644 --- a/src/dds/temp.win.cpp +++ b/src/bpt/temp.win.cpp @@ -1,17 +1,16 @@ -#ifdef _WIN32 #include "./temp.hpp" +#ifdef _WIN32 + #include #include #include -using namespace dds; - -temporary_dir temporary_dir::create() { - auto base = fs::temp_directory_path(); +using namespace bpt; +temporary_dir temporary_dir::create_in(path_ref base) { ::UUID uuid; auto status = ::UuidCreate(&uuid); assert(status == RPC_S_OK || status == RPC_S_UUID_LOCAL_ONLY @@ -22,9 +21,9 @@ temporary_dir temporary_dir::create() { std::string uuid_std_str(reinterpret_cast(uuid_str)); ::RpcStringFree(&uuid_str); - auto new_dir = base / ("dds-" + uuid_std_str); + auto new_dir = base / ("bpt-" + uuid_std_str); std::error_code ec; - fs::create_directory(new_dir); + fs::create_directories(new_dir); return std::make_shared(std::move(new_dir)); } #endif \ No newline at end of file diff --git a/src/bpt/toolchain/errors.hpp b/src/bpt/toolchain/errors.hpp new file mode 100644 index 00000000..3d6deb0b --- /dev/null +++ b/src/bpt/toolchain/errors.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +namespace bpt { + +struct e_toolchain_filepath { + std::filesystem::path value; +}; + +struct e_builtin_toolchain_str { + std::string value; +}; + +} // namespace bpt \ No newline at end of file diff --git a/src/dds/toolchain/from_json.cpp b/src/bpt/toolchain/from_json.cpp similarity index 54% rename from src/dds/toolchain/from_json.cpp rename to src/bpt/toolchain/from_json.cpp index 2fe44653..ffc7cee5 100644 --- a/src/dds/toolchain/from_json.cpp +++ b/src/bpt/toolchain/from_json.cpp @@ -1,18 +1,20 @@ #include "./from_json.hpp" -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include #include #include -#include +#include #include -using namespace dds; +using namespace bpt; using std::optional; using std::string; @@ -40,13 +42,14 @@ template } // namespace -toolchain dds::parse_toolchain_json5(std::string_view j5_str, std::string_view context) { +toolchain bpt::parse_toolchain_json5(std::string_view j5_str, std::string_view context) { auto dat = json5::parse_data(j5_str); return parse_toolchain_json_data(dat, context); } -toolchain dds::parse_toolchain_json_data(const json5::data& dat, std::string_view context) { +toolchain bpt::parse_toolchain_json_data(const json5::data& dat, std::string_view context) { using namespace semester; + using namespace bpt::walk_utils; opt_string compiler_id; opt_string c_compiler; @@ -76,6 +79,9 @@ toolchain dds::parse_toolchain_json_data(const json5::data& dat, std::string_vie opt_string exe_prefix; opt_string exe_suffix; opt_string_seq base_warning_flags; + opt_string_seq base_flags; + opt_string_seq base_c_flags; + opt_string_seq base_cxx_flags; opt_string_seq include_template; opt_string_seq external_include_template; opt_string_seq define_template; @@ -84,7 +90,12 @@ toolchain dds::parse_toolchain_json_data(const json5::data& dat, std::string_vie opt_string_seq create_archive; opt_string_seq link_executable; opt_string_seq tty_flags; + opt_string lang_version_flag_template; + opt_string_seq c_source_type_flags; + opt_string_seq cxx_source_type_flags; + opt_string_seq syntax_only_flags; + opt_string_seq consider_env; // For copy-pasting convenience: ‘{}’ auto extend_flags = [&](string key, auto& opt_flags) { @@ -92,148 +103,140 @@ toolchain dds::parse_toolchain_json_data(const json5::data& dat, std::string_vie if (!opt_flags) { opt_flags.emplace(); } - return decompose( // + walk( // dat, - try_seq{ - if_type([&](auto& str_) { - auto more_flags = split_shell_string(str_.as_string()); - extend(*opt_flags, more_flags); - return dc_accept; - }), - if_array{for_each{ - require_type{ - fmt::format("Elements of `{}` array must be strings", key)}, - write_to{std::back_inserter(*opt_flags)}, - }}, - reject_with{fmt::format("`{}` must be an array or a shell-like string", key)}, - }); + if_type([&](auto& str_) { + auto more_flags = split_shell_string(str_.as_string()); + extend(*opt_flags, more_flags); + return walk.accept; + }), + if_array{for_each{ + require_type{ + fmt::format("Elements of `{}` array must be strings", key)}, + put_into{std::back_inserter(*opt_flags)}, + }}, + reject_with(fmt::format("`{}` must be an array or a shell-like string", key))); + return walk.accept; }; }; -#define KEY_EXTEND_FLAGS(Name) \ - if_key { #Name, extend_flags(#Name, Name) } +#define KEY_EXTEND_FLAGS(Name) if_key{#Name, extend_flags(#Name, Name)} #define KEY_STRING(Name) \ if_key { #Name, require_type < string>("`" #Name "` must be a string"), put_into{Name }, } - auto result = semester::decompose( // + key_dym_tracker base_dym{{ + "compiler_id", + "c_compiler", + "cxx_compiler", + "c_version", + "cxx_version", + "c_flags", + "cxx_flags", + "warning_flags", + "link_flags", + "flags", + "debug", + "optimize", + "runtime", + }}; + + key_dym_tracker adv_dym{{ + "deps_mode", + "include_template", + "external_include_template", + "define_template", + "base_warning_flags", + "base_flags", + "base_c_flags", + "base_cxx_flags", + "c_compile_file", + "cxx_compile_file", + "create_archive", + "link_executable", + "obj_prefix", + "obj_suffix", + "archive_prefix", + "archive_suffix", + "exe_prefix", + "exe_suffix", + "tty_flags", + "lang_version_flag_template", + "c_source_type_flags", + "cxx_source_type_flags", + "syntax_only_flags", + "consider_env", + }}; + + walk( // dat, - try_seq{ - require_type("Root of toolchain data must be a mapping"), - mapping{ - if_key{"$schema", just_accept}, - KEY_STRING(compiler_id), - KEY_STRING(c_compiler), - KEY_STRING(cxx_compiler), - KEY_STRING(c_version), - KEY_STRING(cxx_version), - KEY_EXTEND_FLAGS(c_flags), - KEY_EXTEND_FLAGS(cxx_flags), - KEY_EXTEND_FLAGS(warning_flags), - KEY_EXTEND_FLAGS(link_flags), - KEY_EXTEND_FLAGS(compiler_launcher), - if_key{"debug", - if_type(put_into{debug_bool}), - if_type(put_into{debug_str}), - reject_with{"'debug' must be a bool or string"}}, - if_key{"optimize", - require_type("`optimize` must be a boolean value"), - put_into{do_optimize}}, - if_key{"flags", extend_flags("flags", common_flags)}, - if_key{"runtime", - require_type("'runtime' must be a JSON object"), - mapping{if_key{"static", - require_type("'/runtime/static' should be a boolean"), - put_into(runtime_static)}, - if_key{"debug", - require_type("'/runtime/debug' should be a boolean"), - put_into(runtime_debug)}, - [](auto&& key, auto&&) { - fail("Unknown 'runtime' key '{}'", key); - return dc_reject_t(); - }}}, - if_key{ - "advanced", - require_type("`advanced` must be a mapping"), - mapping{ - if_key{"deps_mode", - require_type("`deps_mode` must be a string"), - put_into{deps_mode_str}}, - KEY_EXTEND_FLAGS(include_template), - KEY_EXTEND_FLAGS(external_include_template), - KEY_EXTEND_FLAGS(define_template), - KEY_EXTEND_FLAGS(base_warning_flags), - KEY_EXTEND_FLAGS(c_compile_file), - KEY_EXTEND_FLAGS(cxx_compile_file), - KEY_EXTEND_FLAGS(create_archive), - KEY_EXTEND_FLAGS(link_executable), - KEY_EXTEND_FLAGS(tty_flags), - KEY_STRING(obj_prefix), - KEY_STRING(obj_suffix), - KEY_STRING(archive_prefix), - KEY_STRING(archive_suffix), - KEY_STRING(exe_prefix), - KEY_STRING(exe_suffix), - [&](auto key, auto) -> dc_reject_t { - auto dym = did_you_mean(key, - { - "deps_mode", - "include_template", - "external_include_template", - "define_template", - "base_warning_flags", - "c_compile_file", - "cxx_compile_file", - "create_archive", - "link_executable", - "obj_prefix", - "obj_suffix", - "archive_prefix", - "archive_suffix", - "exe_prefix", - "exe_suffix", - "tty_flags", - }); - fail(context, - "Unknown toolchain advanced-config key ‘{}’ (Did you mean ‘{}’?)", - key, - *dym); - std::terminate(); - }, - }, - }, - [&](auto key, auto &&) -> dc_reject_t { - // They've given an unknown key. Ouch. - auto dym = did_you_mean(key, - { - "compiler_id", - "c_compiler", - "cxx_compiler", - "c_version", - "cxx_version", - "c_flags", - "cxx_flags", - "warning_flags", - "link_flags", - "flags", - "debug", - "optimize", - "runtime", - }); - fail(context, - "Unknown toolchain config key ‘{}’ (Did you mean ‘{}’?)", - key, - *dym); - std::terminate(); + require_type("Root of toolchain data must be a mapping"), + mapping{ + base_dym.tracker(), + if_key{"$schema", just_accept}, + KEY_STRING(compiler_id), + KEY_STRING(c_compiler), + KEY_STRING(cxx_compiler), + KEY_STRING(c_version), + KEY_STRING(cxx_version), + KEY_EXTEND_FLAGS(c_flags), + KEY_EXTEND_FLAGS(cxx_flags), + KEY_EXTEND_FLAGS(warning_flags), + KEY_EXTEND_FLAGS(link_flags), + KEY_EXTEND_FLAGS(compiler_launcher), + if_key{"debug", + if_type(put_into{debug_bool}), + if_type(put_into{debug_str}), + reject_with("'debug' must be a bool or string")}, + if_key{"optimize", + require_type("`optimize` must be a boolean value"), + put_into{do_optimize}}, + if_key{"flags", extend_flags("flags", common_flags)}, + if_key{"runtime", + require_type("'runtime' must be a JSON object"), + mapping{if_key{"static", + require_type("'/runtime/static' should be a boolean"), + put_into(runtime_static)}, + if_key{"debug", + require_type("'/runtime/debug' should be a boolean"), + put_into(runtime_debug)}}}, + if_key{ + "advanced", + require_type("`advanced` must be a mapping"), + mapping{ + adv_dym.tracker(), + if_key{"deps_mode", + require_type("`deps_mode` must be a string"), + put_into{deps_mode_str}}, + KEY_EXTEND_FLAGS(include_template), + KEY_EXTEND_FLAGS(external_include_template), + KEY_EXTEND_FLAGS(define_template), + KEY_EXTEND_FLAGS(base_warning_flags), + KEY_EXTEND_FLAGS(base_flags), + KEY_EXTEND_FLAGS(base_c_flags), + KEY_EXTEND_FLAGS(base_cxx_flags), + KEY_EXTEND_FLAGS(c_compile_file), + KEY_EXTEND_FLAGS(cxx_compile_file), + KEY_EXTEND_FLAGS(create_archive), + KEY_EXTEND_FLAGS(link_executable), + KEY_EXTEND_FLAGS(tty_flags), + KEY_STRING(obj_prefix), + KEY_STRING(obj_suffix), + KEY_STRING(archive_prefix), + KEY_STRING(archive_suffix), + KEY_STRING(exe_prefix), + KEY_STRING(exe_suffix), + KEY_STRING(lang_version_flag_template), + KEY_EXTEND_FLAGS(c_source_type_flags), + KEY_EXTEND_FLAGS(cxx_source_type_flags), + KEY_EXTEND_FLAGS(syntax_only_flags), + KEY_EXTEND_FLAGS(consider_env), + adv_dym.rejecter(), }, }, + base_dym.rejecter(), }); - auto rej_opt = std::get_if(&result); - if (rej_opt) { - fail(context, rej_opt->message); - } - if (debug_str.has_value() && debug_str != "embedded" && debug_str != "split" && debug_str != "none") { fail(context, "'debug' string must be one of 'none', 'embedded', or 'split'"); @@ -250,7 +253,7 @@ toolchain dds::parse_toolchain_json_data(const json5::data& dat, std::string_vie return no_comp_id; } else if (compiler_id == "msvc") { return msvc; - } else if (compiler_id == "gnu") { + } else if (compiler_id == "gnu" or compiler_id == "gcc") { return gnu; } else if (compiler_id == "clang") { return clang; @@ -264,6 +267,14 @@ toolchain dds::parse_toolchain_json_data(const json5::data& dat, std::string_vie bool is_msvc = compiler_id_e == msvc; bool is_gnu_like = is_gnu || is_clang; + if (!lang_version_flag_template.has_value()) { + if (is_gnu_like) { + lang_version_flag_template = "-std=[version]"; + } else if (is_msvc) { + lang_version_flag_template = "/std:[version]"; + } + } + const enum file_deps_mode deps_mode = [&] { if (!deps_mode_str.has_value()) { if (is_gnu_like) { @@ -313,117 +324,44 @@ toolchain dds::parse_toolchain_json_data(const json5::data& dat, std::string_vie std::terminate(); }; - // Determine the C language version - enum c_version_e_t { - c_none, - c89, - c99, - c11, - c18, - } c_version_e - = [&] { - if (!c_version) { - return c_none; - } else if (c_version == "c89") { - return c89; - } else if (c_version == "c99") { - return c99; - } else if (c_version == "c11") { - return c11; - } else if (c_version == "c18") { - return c18; - } else { - fail(context, "Unknown `c_version` ‘{}’", *c_version); - } - }(); - - enum cxx_version_e_t { - cxx_none, - cxx98, - cxx03, - cxx11, - cxx14, - cxx17, - cxx20, - } cxx_version_e - = [&] { - if (!cxx_version) { - return cxx_none; - } else if (cxx_version == "c++98") { - return cxx98; - } else if (cxx_version == "c++03") { - return cxx03; - } else if (cxx_version == "c++11") { - return cxx11; - } else if (cxx_version == "c++14") { - return cxx14; - } else if (cxx_version == "c++17") { - return cxx17; - } else if (cxx_version == "c++20") { - return cxx20; - } else { - fail(context, "Unknown `cxx_version` ‘{}’", *cxx_version); - } - }(); - - std::map, string_seq> c_version_flag_table = { - {{msvc, c_none}, {}}, - {{msvc, c89}, {}}, - {{msvc, c99}, {}}, - {{msvc, c11}, {}}, - {{msvc, c18}, {}}, - {{gnu, c_none}, {}}, - {{gnu, c89}, {"-std=c89"}}, - {{gnu, c99}, {"-std=c99"}}, - {{gnu, c11}, {"-std=c11"}}, - {{gnu, c18}, {"-std=c18"}}, - {{clang, c_none}, {}}, - {{clang, c89}, {"-std=c89"}}, - {{clang, c99}, {"-std=c99"}}, - {{clang, c11}, {"-std=c11"}}, - {{clang, c18}, {"-std=c18"}}, - }; - auto get_c_version_flags = [&]() -> string_seq { - if (!compiler_id.has_value()) { - fail(context, "Unable to deduce flags for 'c_version' without setting 'compiler_id'"); + if (!c_version) { + return {}; } - auto c_ver_iter = c_version_flag_table.find({compiler_id_e, c_version_e}); - assert(c_ver_iter != c_version_flag_table.end()); - return c_ver_iter->second; - }; - - std::map, string_seq> cxx_version_flag_table = { - {{msvc, cxx_none}, {}}, - {{msvc, cxx98}, {}}, - {{msvc, cxx03}, {}}, - {{msvc, cxx11}, {}}, - {{msvc, cxx14}, {"/std:c++14"}}, - {{msvc, cxx17}, {"/std:c++17"}}, - {{msvc, cxx20}, {"/std:c++latest"}}, - {{gnu, cxx_none}, {}}, - {{gnu, cxx98}, {"-std=c++98"}}, - {{gnu, cxx03}, {"-std=c++03"}}, - {{gnu, cxx11}, {"-std=c++11"}}, - {{gnu, cxx14}, {"-std=c++14"}}, - {{gnu, cxx17}, {"-std=c++17"}}, - {{gnu, cxx20}, {"-std=c++20"}}, - {{clang, cxx_none}, {}}, - {{clang, cxx98}, {"-std=c++98"}}, - {{clang, cxx03}, {"-std=c++03"}}, - {{clang, cxx11}, {"-std=c++11"}}, - {{clang, cxx14}, {"-std=c++14"}}, - {{clang, cxx17}, {"-std=c++17"}}, - {{clang, cxx20}, {"-std=c++20"}}, + if (!lang_version_flag_template) { + if (!compiler_id) { + fail(context, + "Unable to deduce flags for 'c_version' without setting 'compiler_id' or " + "'lang_version_flag_template'"); + } else { + fail(context, + "Unable to determine the 'lang_version_flag_template' for the given " + "'compiler_id', required to generate the language version flags for " + "'c_version'"); + } + } + auto flag = replace(*lang_version_flag_template, "[version]", *c_version); + return {flag}; }; auto get_cxx_version_flags = [&]() -> string_seq { - if (!compiler_id.has_value()) { - fail(context, "Unable to deduce flags for 'cxx_version' without setting 'compiler_id'"); + if (!cxx_version) { + return {}; + } + if (!lang_version_flag_template) { + if (!compiler_id) { + fail(context, + "Unable to deduce flags for 'cxx_version' without setting 'compiler_id' or " + "'lang_version_flag_template'"); + } else { + fail(context, + "Unable to determine the 'lang_version_flag_template' for the given " + "'compiler_id', required to generate the language version flags for " + "'cxx_version'"); + } } - auto cxx_ver_iter = cxx_version_flag_table.find({compiler_id_e, cxx_version_e}); - assert(cxx_ver_iter != cxx_version_flag_table.end()); - return cxx_ver_iter->second; + auto flag = replace(*lang_version_flag_template, "[version]", *cxx_version); + return {flag}; }; auto get_runtime_flags = [&]() -> string_seq { @@ -504,34 +442,59 @@ toolchain dds::parse_toolchain_json_data(const json5::data& dat, std::string_vie return ret; }; - auto get_flags = [&](language lang) -> string_seq { + auto get_default_compile_command = [&]() -> string_seq { string_seq ret; - extend(ret, get_runtime_flags()); - extend(ret, get_optim_flags()); - extend(ret, get_debug_flags()); if (is_msvc) { - if (lang == language::cxx) { + extend(ret, {"[flags]", "/c", "[in]", "/Fo[out]"}); + } else if (is_gnu_like) { + extend(ret, {"[flags]", "-c", "[in]", "-o[out]"}); + } + return ret; + }; + + auto get_base_flags = [&](language lang) -> string_seq { + string_seq ret; + if (base_flags) { + extend(ret, *base_flags); + } else if (is_msvc) { + extend(ret, {"/nologo", "/permissive-"}); + } else if (is_gnu_like) { + extend(ret, {"-fPIC", "-pthread"}); + } + if (lang == language::c && base_c_flags) { + extend(ret, *base_c_flags); + } + if (lang == language::cxx) { + if (base_cxx_flags) { + extend(ret, *base_cxx_flags); + } else if (is_msvc) { extend(ret, {"/EHsc"}); } - extend(ret, {"/nologo", "/permissive-", "[flags]", "/c", "[in]", "/Fo[out]"}); - } else if (is_gnu_like) { - extend(ret, {"-fPIC", "-pthread", "[flags]", "-c", "[in]", "-o[out]"}); } + return ret; + }; + + auto get_flags = [&](language lang) -> string_seq { + string_seq ret; + extend(ret, get_runtime_flags()); + extend(ret, get_optim_flags()); + extend(ret, get_debug_flags()); if (common_flags) { extend(ret, *common_flags); } - if (lang == language::cxx && cxx_flags) { - extend(ret, *cxx_flags); - } - if (lang == language::cxx && cxx_version) { + if (lang == language::cxx) { + if (cxx_flags) { + extend(ret, *cxx_flags); + } extend(ret, get_cxx_version_flags()); } - if (lang == language::c && c_flags) { - extend(ret, *c_flags); - } - if (lang == language::c && c_version) { + if (lang == language::c) { + if (c_flags) { + extend(ret, *c_flags); + } extend(ret, get_c_version_flags()); } + extend(ret, get_base_flags(lang)); return ret; }; @@ -543,9 +506,10 @@ toolchain dds::parse_toolchain_json_data(const json5::data& dat, std::string_vie extend(c, *compiler_launcher); } c.push_back(get_compiler_executable_path(language::c)); - extend(c, get_flags(language::c)); + extend(c, get_default_compile_command()); return c; }); + extend(tc.c_compile, get_flags(language::c)); tc.cxx_compile = read_opt(cxx_compile_file, [&] { string_seq cxx; @@ -553,9 +517,17 @@ toolchain dds::parse_toolchain_json_data(const json5::data& dat, std::string_vie extend(cxx, *compiler_launcher); } cxx.push_back(get_compiler_executable_path(language::cxx)); - extend(cxx, get_flags(language::cxx)); + extend(cxx, get_default_compile_command()); return cxx; }); + extend(tc.cxx_compile, get_flags(language::cxx)); + + tc.consider_envs = read_opt(consider_env, [&]() -> string_seq { + if (is_msvc) { + return {"CL", "_CL_", "INCLUDE", "LIBPATH", "LIB"}; + } + return {}; + }); tc.include_template = read_opt(include_template, [&]() -> string_seq { if (!compiler_id) { @@ -690,9 +662,9 @@ toolchain dds::parse_toolchain_json_data(const json5::data& dat, std::string_vie assert(false && "No link-exe command"); std::terminate(); } - extend(ret, get_link_flags()); return ret; }); + extend(tc.link_exe, get_link_flags()); tc.tty_flags = read_opt(tty_flags, [&]() -> string_seq { if (!compiler_id) { @@ -711,5 +683,47 @@ toolchain dds::parse_toolchain_json_data(const json5::data& dat, std::string_vie } }); + tc.c_source_type_flags = read_opt(c_source_type_flags, [&]() -> string_seq { + if (!compiler_id) { + fail(context, "Unable to deduce C source type flags without a 'compiler_id'"); + } + if (is_msvc) { + return {"/TC"}; + } else if (is_gnu_like) { + return {"-xc"}; + } else { + assert(false && "Impossible compiler_id while deducing `c_source_type_flags`"); + std::terminate(); + } + }); + + tc.cxx_source_type_flags = read_opt(cxx_source_type_flags, [&]() -> string_seq { + if (!compiler_id) { + fail(context, "Unable to deduce C++ source type flags without a 'compiler_id'"); + } + if (is_msvc) { + return {"/TP"}; + } else if (is_gnu_like) { + return {"-xc++"}; + } else { + assert(false && "Impossible compiler_id while deducing `cxx_source_type_flags`"); + std::terminate(); + } + }); + + tc.syntax_only_flags = read_opt(syntax_only_flags, [&]() -> string_seq { + if (!compiler_id) { + fail(context, "Unable to deduce C++ syntax only flags without a 'compiler_id'"); + } + if (is_msvc) { + return {"/Zs"}; + } else if (is_gnu_like) { + return {"-fsyntax-only"}; + } else { + assert(false && "Impossible compiler_id while deducing `syntax_only_flags`"); + std::terminate(); + } + }); + return tc.realize(); -} \ No newline at end of file +} diff --git a/src/dds/toolchain/from_json.hpp b/src/bpt/toolchain/from_json.hpp similarity index 65% rename from src/dds/toolchain/from_json.hpp rename to src/bpt/toolchain/from_json.hpp index ab2169b6..53802edc 100644 --- a/src/dds/toolchain/from_json.hpp +++ b/src/bpt/toolchain/from_json.hpp @@ -1,12 +1,17 @@ #pragma once -#include +#include +#include #include #include -namespace dds { +namespace bpt { + +struct e_bad_toolchain_key : e_nonesuch { + using e_nonesuch::e_nonesuch; +}; toolchain parse_toolchain_json5(std::string_view json5, std::string_view context = "Loading toolchain JSON"); @@ -14,4 +19,4 @@ toolchain parse_toolchain_json5(std::string_view json5, toolchain parse_toolchain_json_data(const json5::data& data, std::string_view context = "Loading toolchain JSON"); -} // namespace dds \ No newline at end of file +} // namespace bpt \ No newline at end of file diff --git a/src/bpt/toolchain/from_json.test.cpp b/src/bpt/toolchain/from_json.test.cpp new file mode 100644 index 00000000..62934eba --- /dev/null +++ b/src/bpt/toolchain/from_json.test.cpp @@ -0,0 +1,313 @@ +#include + +#include + +#include + +namespace { + +struct test { + std::string_view given; + std::string_view compile; + std::string_view with_warnings; + std::string_view ar; + std::string_view link; +}; + +void check_tc_compile(struct test test) { + auto tc = bpt::parse_toolchain_json5(test.given); + + bpt::compile_file_spec cf; + cf.source_path = "foo.cpp"; + cf.out_path = "foo.o"; + auto cf_cmd = tc.create_compile_command(cf, bpt::fs::current_path(), bpt::toolchain_knobs{}); + auto cf_cmd_str = bpt::quote_command(cf_cmd.command); + CHECK(cf_cmd_str == test.compile); + + cf.enable_warnings = true; + cf_cmd = tc.create_compile_command(cf, bpt::fs::current_path(), bpt::toolchain_knobs{}); + cf_cmd_str = bpt::quote_command(cf_cmd.command); + CHECK(cf_cmd_str == test.with_warnings); + + bpt::archive_spec ar_spec; + ar_spec.input_files.push_back("foo.o"); + ar_spec.input_files.push_back("bar.o"); + ar_spec.out_path = "stuff.a"; + auto ar_cmd + = tc.create_archive_command(ar_spec, bpt::fs::current_path(), bpt::toolchain_knobs{}); + auto ar_cmd_str = bpt::quote_command(ar_cmd); + CHECK(ar_cmd_str == test.ar); + + bpt::link_exe_spec exe_spec; + exe_spec.inputs.push_back("foo.o"); + exe_spec.inputs.push_back("bar.a"); + exe_spec.output = "meow.exe"; + auto exe_cmd = tc.create_link_executable_command(exe_spec, + bpt::fs::current_path(), + bpt::toolchain_knobs{}); + auto exe_cmd_str = bpt::quote_command(exe_cmd); + CHECK(exe_cmd_str == test.link); +} + +} // namespace + +TEST_CASE("Generating toolchain commands") { + check_tc_compile(test{ + .given = "{compiler_id: 'gnu'}", + .compile = "g++ -MD -MF foo.o.d -MQ foo.o -c foo.cpp -ofoo.o -fPIC -pthread", + .with_warnings = "g++ -Wall -Wextra -Wpedantic -Wconversion -MD -MF foo.o.d -MQ " + "foo.o -c foo.cpp -ofoo.o -fPIC -pthread", + .ar = "ar rcs stuff.a foo.o bar.o", + .link = "g++ -fPIC foo.o bar.a -pthread -omeow.exe", + }); + + check_tc_compile(test{ + .given = "{compiler_id: 'gnu', debug: true}", + .compile = "g++ -MD -MF foo.o.d -MQ foo.o -c foo.cpp -ofoo.o -g -fPIC -pthread", + .with_warnings = "g++ -Wall -Wextra -Wpedantic -Wconversion -MD -MF foo.o.d -MQ " + "foo.o -c foo.cpp -ofoo.o -g -fPIC -pthread", + .ar = "ar rcs stuff.a foo.o bar.o", + .link = "g++ -fPIC foo.o bar.a -pthread -omeow.exe -g", + }); + + check_tc_compile(test{ + .given = "{compiler_id: 'gnu', debug: true, optimize: true}", + .compile = "g++ -MD -MF foo.o.d -MQ foo.o -c foo.cpp -ofoo.o -O2 -g -fPIC -pthread", + .with_warnings = "g++ -Wall -Wextra -Wpedantic -Wconversion -MD -MF foo.o.d -MQ " + "foo.o -c foo.cpp -ofoo.o -O2 -g -fPIC -pthread", + .ar = "ar rcs stuff.a foo.o bar.o", + .link = "g++ -fPIC foo.o bar.a -pthread -omeow.exe -O2 -g", + }); + + check_tc_compile(test{ + .given = "{compiler_id: 'gnu', debug: 'split', optimize: true}", + .compile + = "g++ -MD -MF foo.o.d -MQ foo.o -c foo.cpp -ofoo.o -O2 -g -gsplit-dwarf -fPIC -pthread", + .with_warnings = "g++ -Wall -Wextra -Wpedantic -Wconversion -MD -MF foo.o.d -MQ " + "foo.o -c foo.cpp -ofoo.o -O2 -g -gsplit-dwarf -fPIC -pthread", + .ar = "ar rcs stuff.a foo.o bar.o", + .link = "g++ -fPIC foo.o bar.a -pthread -omeow.exe -O2 -g -gsplit-dwarf", + }); + + check_tc_compile(test{ + .given = "{compiler_id: 'gnu', cxx_version: 'gnu++20'}", + .compile = "g++ -MD -MF foo.o.d -MQ foo.o -c foo.cpp -ofoo.o -std=gnu++20 -fPIC -pthread", + .with_warnings = "g++ -Wall -Wextra -Wpedantic -Wconversion -MD -MF foo.o.d -MQ " + "foo.o -c foo.cpp -ofoo.o -std=gnu++20 -fPIC -pthread", + .ar = "ar rcs stuff.a foo.o bar.o", + .link = "g++ -fPIC foo.o bar.a -pthread -omeow.exe", + }); + + check_tc_compile(test{ + .given = "{compiler_id: 'gnu', flags: '-fno-rtti', advanced: {cxx_compile_file: 'g++ " + "[flags] -c [in] -o[out]'}}", + .compile = "g++ -MD -MF foo.o.d -MQ foo.o -c foo.cpp -ofoo.o -fno-rtti -fPIC -pthread", + .with_warnings = "g++ -Wall -Wextra -Wpedantic -Wconversion -MD -MF foo.o.d -MQ " + "foo.o -c foo.cpp -ofoo.o -fno-rtti -fPIC -pthread", + .ar = "ar rcs stuff.a foo.o bar.o", + .link = "g++ -fPIC foo.o bar.a -pthread -omeow.exe", + }); + + check_tc_compile(test{ + .given + = "{compiler_id: 'gnu', flags: '-fno-rtti', advanced: {base_flags: '-fno-exceptions'}}", + .compile = "g++ -MD -MF foo.o.d -MQ foo.o -c foo.cpp -ofoo.o -fno-rtti -fno-exceptions", + .with_warnings = "g++ -Wall -Wextra -Wpedantic -Wconversion -MD -MF foo.o.d -MQ " + "foo.o -c foo.cpp -ofoo.o -fno-rtti -fno-exceptions", + .ar = "ar rcs stuff.a foo.o bar.o", + .link = "g++ -fPIC foo.o bar.a -pthread -omeow.exe", + }); + + check_tc_compile(test{ + .given = "{compiler_id: 'gnu', flags: '-ansi', cxx_flags: '-fno-rtti', advanced: " + "{base_flags: '-fno-builtin', base_cxx_flags: '-fno-exceptions'}}", + .compile = "g++ -MD -MF foo.o.d -MQ foo.o -c foo.cpp -ofoo.o -ansi -fno-rtti " + "-fno-builtin -fno-exceptions", + .with_warnings + = "g++ -Wall -Wextra -Wpedantic -Wconversion -MD -MF foo.o.d -MQ foo.o -c foo.cpp -ofoo.o " + "-ansi -fno-rtti -fno-builtin -fno-exceptions", + .ar = "ar rcs stuff.a foo.o bar.o", + .link = "g++ -fPIC foo.o bar.a -pthread -omeow.exe", + }); + + check_tc_compile(test{ + .given = "{compiler_id: 'gnu', link_flags: '-mthumb'}", + .compile = "g++ -MD -MF foo.o.d -MQ foo.o -c foo.cpp -ofoo.o -fPIC -pthread", + .with_warnings = "g++ -Wall -Wextra -Wpedantic -Wconversion -MD -MF foo.o.d -MQ " + "foo.o -c foo.cpp -ofoo.o -fPIC -pthread", + .ar = "ar rcs stuff.a foo.o bar.o", + .link = "g++ -fPIC foo.o bar.a -pthread -omeow.exe -mthumb", + }); + + check_tc_compile(test{ + .given = "{compiler_id: 'gnu', link_flags: '-mthumb', advanced: {link_executable: 'g++ " + "[in] -o[out]'}}", + .compile = "g++ -MD -MF foo.o.d -MQ foo.o -c foo.cpp -ofoo.o -fPIC -pthread", + .with_warnings = "g++ -Wall -Wextra -Wpedantic -Wconversion -MD -MF foo.o.d -MQ " + "foo.o -c foo.cpp -ofoo.o -fPIC -pthread", + .ar = "ar rcs stuff.a foo.o bar.o", + .link = "g++ foo.o bar.a -omeow.exe -mthumb", + }); + + check_tc_compile(test{ + .given = "{compiler_id: 'msvc'}", + .compile = "cl.exe /showIncludes /c foo.cpp /Fofoo.o /MT /nologo /permissive- /EHsc", + .with_warnings + = "cl.exe /W4 /showIncludes /c foo.cpp /Fofoo.o /MT /nologo /permissive- /EHsc", + .ar = "lib /nologo /OUT:stuff.a foo.o bar.o", + .link = "cl.exe /nologo /EHsc foo.o bar.a /Femeow.exe /MT", + }); + + check_tc_compile(test{ + .given = "{compiler_id: 'msvc', debug: true}", + .compile = "cl.exe /showIncludes /c foo.cpp /Fofoo.o /MTd /Z7 /nologo /permissive- /EHsc", + .with_warnings + = "cl.exe /W4 /showIncludes /c foo.cpp /Fofoo.o /MTd /Z7 /nologo /permissive- /EHsc", + .ar = "lib /nologo /OUT:stuff.a foo.o bar.o", + .link = "cl.exe /nologo /EHsc foo.o bar.a /Femeow.exe /MTd /Z7", + }); + + check_tc_compile(test{ + .given = "{compiler_id: 'msvc', debug: 'embedded'}", + .compile = "cl.exe /showIncludes /c foo.cpp /Fofoo.o /MTd /Z7 /nologo /permissive- /EHsc", + .with_warnings + = "cl.exe /W4 /showIncludes /c foo.cpp /Fofoo.o /MTd /Z7 /nologo /permissive- /EHsc", + .ar = "lib /nologo /OUT:stuff.a foo.o bar.o", + .link = "cl.exe /nologo /EHsc foo.o bar.a /Femeow.exe /MTd /Z7", + }); + + check_tc_compile(test{ + .given = "{compiler_id: 'msvc', debug: 'split'}", + .compile + = "cl.exe /showIncludes /c foo.cpp /Fofoo.o /MTd /Zi /FS /nologo /permissive- /EHsc", + .with_warnings + = "cl.exe /W4 /showIncludes /c foo.cpp /Fofoo.o /MTd /Zi /FS /nologo /permissive- /EHsc", + .ar = "lib /nologo /OUT:stuff.a foo.o bar.o", + .link = "cl.exe /nologo /EHsc foo.o bar.a /Femeow.exe /MTd /Zi /FS", + }); + + check_tc_compile(test{ + .given = "{compiler_id: 'msvc', flags: '-DFOO'}", + .compile = "cl.exe /showIncludes /c foo.cpp /Fofoo.o /MT -DFOO /nologo /permissive- /EHsc", + .with_warnings + = "cl.exe /W4 /showIncludes /c foo.cpp /Fofoo.o /MT -DFOO /nologo /permissive- /EHsc", + .ar = "lib /nologo /OUT:stuff.a foo.o bar.o", + .link = "cl.exe /nologo /EHsc foo.o bar.a /Femeow.exe /MT", + }); + + check_tc_compile(test{ + .given = "{compiler_id: 'msvc', runtime: {static: false}}", + .compile = "cl.exe /showIncludes /c foo.cpp /Fofoo.o /MD /nologo /permissive- /EHsc", + .with_warnings + = "cl.exe /W4 /showIncludes /c foo.cpp /Fofoo.o /MD /nologo /permissive- /EHsc", + .ar = "lib /nologo /OUT:stuff.a foo.o bar.o", + .link = "cl.exe /nologo /EHsc foo.o bar.a /Femeow.exe /MD", + }); + + check_tc_compile(test{ + .given = "{compiler_id: 'msvc', runtime: {static: false}, debug: true}", + .compile = "cl.exe /showIncludes /c foo.cpp /Fofoo.o /MDd /Z7 /nologo /permissive- /EHsc", + .with_warnings + = "cl.exe /W4 /showIncludes /c foo.cpp /Fofoo.o /MDd /Z7 /nologo /permissive- /EHsc", + .ar = "lib /nologo /OUT:stuff.a foo.o bar.o", + .link = "cl.exe /nologo /EHsc foo.o bar.a /Femeow.exe /MDd /Z7", + }); + + check_tc_compile(test{ + .given = "{compiler_id: 'msvc', runtime: {static: false, debug: true}}", + .compile = "cl.exe /showIncludes /c foo.cpp /Fofoo.o /MDd /nologo /permissive- /EHsc", + .with_warnings + = "cl.exe /W4 /showIncludes /c foo.cpp /Fofoo.o /MDd /nologo /permissive- /EHsc", + .ar = "lib /nologo /OUT:stuff.a foo.o bar.o", + .link = "cl.exe /nologo /EHsc foo.o bar.a /Femeow.exe /MDd", + }); + + check_tc_compile(test{ + .given = "{compiler_id: 'msvc', advanced: {base_cxx_flags: ''}}", + .compile = "cl.exe /showIncludes /c foo.cpp /Fofoo.o /MT /nologo /permissive-", + .with_warnings = "cl.exe /W4 /showIncludes /c foo.cpp /Fofoo.o /MT /nologo /permissive-", + .ar = "lib /nologo /OUT:stuff.a foo.o bar.o", + .link = "cl.exe /nologo /EHsc foo.o bar.a /Femeow.exe /MT", + }); + + check_tc_compile(test{ + .given = "{compiler_id: 'msvc', cxx_version: 'c++latest'}", + .compile + = "cl.exe /showIncludes /c foo.cpp /Fofoo.o /MT /std:c++latest /nologo /permissive- /EHsc", + .with_warnings = "cl.exe /W4 /showIncludes /c foo.cpp /Fofoo.o /MT /std:c++latest " + "/nologo /permissive- /EHsc", + .ar = "lib /nologo /OUT:stuff.a foo.o bar.o", + .link = "cl.exe /nologo /EHsc foo.o bar.a /Femeow.exe /MT", + }); + + check_tc_compile(test{ + .given = "{compiler_id: 'msvc', advanced: {lang_version_flag_template: '/eggs:[version]'}, " + "cxx_version: 'meow'}", + .compile + = "cl.exe /showIncludes /c foo.cpp /Fofoo.o /MT /eggs:meow /nologo /permissive- /EHsc", + .with_warnings + = "cl.exe /W4 /showIncludes /c foo.cpp /Fofoo.o /MT /eggs:meow /nologo /permissive- /EHsc", + .ar = "lib /nologo /OUT:stuff.a foo.o bar.o", + .link = "cl.exe /nologo /EHsc foo.o bar.a /Femeow.exe /MT", + }); +} + +TEST_CASE("Manipulate a toolchain and file compilation") { + auto tc = bpt::parse_toolchain_json5("{compiler_id: 'gnu'}"); + + bpt::compile_file_spec cfs; + cfs.source_path = "foo.cpp"; + cfs.out_path = "foo.o"; + auto cmd = tc.create_compile_command(cfs, bpt::fs::current_path(), bpt::toolchain_knobs{}); + CHECK(cmd.command + == std::vector{"g++", + "-MD", + "-MF", + "foo.o.d", + "-MQ", + "foo.o", + "-c", + "foo.cpp", + "-ofoo.o", + "-fPIC", + "-pthread"}); + + cfs.definitions.push_back("FOO=BAR"); + cmd = tc.create_compile_command(cfs, + bpt::fs::current_path(), + bpt::toolchain_knobs{.is_tty = true}); + CHECK(cmd.command + == std::vector{"g++", + "-fdiagnostics-color", + "-D", + "FOO=BAR", + "-MD", + "-MF", + "foo.o.d", + "-MQ", + "foo.o", + "-c", + "foo.cpp", + "-ofoo.o", + "-fPIC", + "-pthread"}); + + cfs.include_dirs.push_back("fake-dir"); + cmd = tc.create_compile_command(cfs, bpt::fs::current_path(), bpt::toolchain_knobs{}); + CHECK(cmd.command + == std::vector{"g++", + "-I", + "fake-dir", + "-D", + "FOO=BAR", + "-MD", + "-MF", + "foo.o.d", + "-MQ", + "foo.o", + "-c", + "foo.cpp", + "-ofoo.o", + "-fPIC", + "-pthread"}); +} diff --git a/src/bpt/toolchain/prep.cpp b/src/bpt/toolchain/prep.cpp new file mode 100644 index 00000000..90f63b8f --- /dev/null +++ b/src/bpt/toolchain/prep.cpp @@ -0,0 +1,52 @@ +#include "./prep.hpp" + +#include +#include +#include +#include + +#include +#include +#include + +#include + +using namespace bpt; +using json = nlohmann::json; + +toolchain toolchain_prep::realize() const { return toolchain::realize(*this); } + +std::uint64_t toolchain_prep::compute_hash() const noexcept { + auto should_prune_flag = [](std::string_view v) { + bool prune = v == neo::oper::any_of("-fdiagnostics-color", "/nologo"); + if (prune) { + return prune; + } else if (v.starts_with("-fconcept-diagnostics-depth=")) { + return true; + } else { + return false; + } + }; + auto prune_flags = [&](auto&& arr) { + return arr // + | std::views::filter([&](auto&& s) { return !should_prune_flag(s); }) + | ranges::v3::to_vector; + }; + + // Only convert the details relevant to the ABI + auto root = json::object({ + {"c_compile", json(prune_flags(c_compile))}, + {"cxx_compile", json(prune_flags(cxx_compile))}, + }); + auto env = json::object(); + for (auto& varname : consider_envs) { + auto val = bpt::getenv(varname); + if (val) { + env[varname] = *val; + } + } + root["env"] = env; + // Make a very normalized document + std::string s = root.dump(); + return bpt::siphash64(42, 1729, neo::const_buffer(s)).digest(); +} diff --git a/src/dds/toolchain/prep.hpp b/src/bpt/toolchain/prep.hpp similarity index 60% rename from src/dds/toolchain/prep.hpp rename to src/bpt/toolchain/prep.hpp index 30019d6c..b1905da3 100644 --- a/src/dds/toolchain/prep.hpp +++ b/src/bpt/toolchain/prep.hpp @@ -1,11 +1,11 @@ #pragma once -#include +#include #include #include -namespace dds { +namespace bpt { class toolchain; @@ -21,6 +21,13 @@ struct toolchain_prep { string_seq warning_flags; string_seq tty_flags; + // Environment variables that should be taken into consideration when generating a toolchain ID + string_seq consider_envs; + + string_seq c_source_type_flags; + string_seq cxx_source_type_flags; + string_seq syntax_only_flags; + std::string archive_prefix; std::string archive_suffix; std::string object_prefix; @@ -30,7 +37,9 @@ struct toolchain_prep { enum file_deps_mode deps_mode; - toolchain realize() const; + [[nodiscard]] toolchain realize() const; + + [[nodiscard]] std::uint64_t compute_hash() const noexcept; }; -} // namespace dds +} // namespace bpt diff --git a/src/bpt/toolchain/prep.test.cpp b/src/bpt/toolchain/prep.test.cpp new file mode 100644 index 00000000..b4aac207 --- /dev/null +++ b/src/bpt/toolchain/prep.test.cpp @@ -0,0 +1,14 @@ +#include "./prep.hpp" + +#include + +TEST_CASE("Hash the toolchain") { + bpt::toolchain_prep prep; + CHECK(prep.compute_hash() == 0x174b6917312d24b2); + + prep.c_compile = {"gcc"}; + CHECK(prep.compute_hash() == 0x5ba3168895eae55a); + // Some compiler options are pruned as part of the hash + prep.c_compile = {"gcc", "-fdiagnostics-color"}; + CHECK(prep.compute_hash() == 0x5ba3168895eae55a); +} diff --git a/src/dds/toolchain/toolchain.cpp b/src/bpt/toolchain/toolchain.cpp similarity index 65% rename from src/dds/toolchain/toolchain.cpp rename to src/bpt/toolchain/toolchain.cpp index 55dfbc8f..861edb29 100644 --- a/src/dds/toolchain/toolchain.cpp +++ b/src/bpt/toolchain/toolchain.cpp @@ -1,20 +1,33 @@ #include "./toolchain.hpp" -#include -#include -#include -#include -#include -#include - +#include "./errors.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include #include #include +#include #include #include #include -using namespace dds; +using namespace bpt; using std::optional; using std::string; @@ -40,6 +53,13 @@ toolchain toolchain::realize(const toolchain_prep& prep) { ret._exe_suffix = prep.exe_suffix; ret._deps_mode = prep.deps_mode; ret._tty_flags = prep.tty_flags; + + ret._c_source_type_flags = prep.c_source_type_flags; + ret._cxx_source_type_flags = prep.cxx_source_type_flags; + ret._syntax_only_flags = prep.syntax_only_flags; + + ret._hash = prep.compute_hash(); + return ret; } @@ -77,7 +97,9 @@ compile_command_info toolchain::create_compile_command(const compile_file_spec& toolchain_knobs knobs) const noexcept { using namespace std::literals; - dds_log(trace, + fs::path in_file = spec.source_path; + + bpt_log(trace, "Calculate compile command for source file [{}] to object file [{}]", spec.source_path.string(), spec.out_path.string()); @@ -93,33 +115,47 @@ compile_command_info toolchain::create_compile_command(const compile_file_spec& vector flags; if (knobs.is_tty) { - dds_log(trace, "Enabling TTY flags."); + bpt_log(trace, "Enabling TTY flags."); extend(flags, _tty_flags); } if (knobs.cache_buster) { // This is simply a CPP definition that is used to "bust" any caches that rely on inspecting // the command-line of the compiler (including our own). - auto def = replace(_def_template, "[def]", "__dds_cachebust=" + *knobs.cache_buster); + auto def = replace(_def_template, "[def]", "__bpt_cachebust=" + *knobs.cache_buster); extend(flags, def); } - dds_log(trace, "#include-search dirs:"); + if (spec.syntax_only) { + bpt_log(trace, "Enabling syntax-only mode"); + extend(flags, _syntax_only_flags); + extend(flags, lang == language::c ? _c_source_type_flags : _cxx_source_type_flags); + + in_file = spec.out_path.parent_path() / spec.source_path.filename(); + in_file += ".syncheck"; + bpt_log(trace, "Syntax check file: {}", in_file); + + fs::create_directories(in_file.parent_path()); + auto abs_path = bpt::resolve_path_weak(spec.source_path); + bpt::write_file(in_file, fmt::format("#include \"{}\"", abs_path.string())); + } + + bpt_log(trace, "#include search-dirs:"); for (auto&& inc_dir : spec.include_dirs) { - dds_log(trace, " - search: {}", inc_dir.string()); + bpt_log(trace, " - search: {}", inc_dir.string()); auto shortest = shortest_path_from(inc_dir, cwd); auto inc_args = include_args(shortest); extend(flags, inc_args); } for (auto&& ext_inc_dir : spec.external_include_dirs) { - dds_log(trace, " - search (external): {}", ext_inc_dir.string()); + bpt_log(trace, " - search (external): {}", ext_inc_dir.string()); auto inc_args = external_include_args(ext_inc_dir); extend(flags, inc_args); } if (knobs.tweaks_dir) { - dds_log(trace, " - search (tweaks): {}", knobs.tweaks_dir->string()); + bpt_log(trace, " - search (tweaks): {}", knobs.tweaks_dir->string()); auto shortest = shortest_path_from(*knobs.tweaks_dir, cwd); auto tweak_inc_args = include_args(shortest); extend(flags, tweak_inc_args); @@ -155,25 +191,25 @@ compile_command_info toolchain::create_compile_command(const compile_file_spec& if (arg == "[flags]") { extend(command, flags); } else { - arg = replace(arg, "[in]", spec.source_path.string()); + arg = replace(arg, "[in]", in_file.string()); arg = replace(arg, "[out]", spec.out_path.string()); command.push_back(arg); } } - return {command, gnu_depfile_path}; + return {std::move(command), std::move(gnu_depfile_path)}; } vector toolchain::create_archive_command(const archive_spec& spec, path_ref cwd, toolchain_knobs) const noexcept { vector cmd; - dds_log(trace, "Creating archive command [output: {}]", spec.out_path.string()); + bpt_log(trace, "Creating archive command [output: {}]", spec.out_path.string()); auto out_arg = shortest_path_from(spec.out_path, cwd).string(); for (auto& arg : _link_archive) { if (arg == "[in]") { - dds_log(trace, "Expand [in] placeholder:"); + bpt_log(trace, "Expand [in] placeholder:"); for (auto&& in : spec.input_files) { - dds_log(trace, " - input: [{}]", in.string()); + bpt_log(trace, " - input: [{}]", in.string()); } extend(cmd, shortest_path_args(cwd, spec.input_files)); } else { @@ -187,12 +223,12 @@ vector toolchain::create_link_executable_command(const link_exe_spec& sp path_ref cwd, toolchain_knobs) const noexcept { vector cmd; - dds_log(trace, "Creating link command [output: {}]", spec.output.string()); + bpt_log(trace, "Creating link command [output: {}]", spec.output.string()); for (auto& arg : _link_exe) { if (arg == "[in]") { - dds_log(trace, "Expand [in] placeholder:"); + bpt_log(trace, "Expand [in] placeholder:"); for (auto&& in : spec.inputs) { - dds_log(trace, " - input: [{}]", in.string()); + bpt_log(trace, " - input: [{}]", in.string()); } extend(cmd, shortest_path_args(cwd, spec.inputs)); } else { @@ -202,35 +238,38 @@ vector toolchain::create_link_executable_command(const link_exe_spec& sp return cmd; } -std::optional toolchain::get_builtin(std::string_view tc_id) noexcept { +toolchain toolchain::get_builtin(const std::string_view tc_id_) { + BPT_E_SCOPE(bpt::e_builtin_toolchain_str{std::string(tc_id_)}); + auto tc_id = tc_id_; using namespace std::literals; json5::data tc_data = json5::data::mapping_type(); auto& root_map = tc_data.as_object(); + auto handle_prefix = [&](std::string_view key, std::string_view prefix) { + assert(!prefix.empty()); + assert(prefix.back() == ':'); + if (starts_with(tc_id, prefix)) { + tc_id.remove_prefix(prefix.length()); + prefix.remove_suffix(1); // remove trailing : + root_map.emplace(key, std::string(prefix)); + return true; + } + return false; + }; + if (starts_with(tc_id, "debug:")) { tc_id = tc_id.substr("debug:"sv.length()); root_map.emplace("debug", true); } - if (starts_with(tc_id, "ccache:")) { - tc_id = tc_id.substr("ccache:"sv.length()); - root_map.emplace("compiler_launcher", "ccache"); - } + handle_prefix("compiler_launcher", "ccache:"); -#define CXX_VER_TAG(str, version) \ - if (starts_with(tc_id, str)) { \ - tc_id = tc_id.substr(std::string_view(str).length()); \ - root_map.emplace("cxx_version", version); \ - } \ - static_assert(true) - - CXX_VER_TAG("c++98:", "c++98"); - CXX_VER_TAG("c++03:", "c++03"); - CXX_VER_TAG("c++11:", "c++11"); - CXX_VER_TAG("c++14:", "c++14"); - CXX_VER_TAG("c++17:", "c++17"); - CXX_VER_TAG("c++20:", "c++20"); + for (std::string_view prefix : {"c++98:", "c++03:", "c++11:", "c++14:", "c++17:", "c++20:"}) { + if (handle_prefix("cxx_version", prefix)) { + break; + } + } struct compiler_info { string c; @@ -286,7 +325,9 @@ std::optional toolchain::get_builtin(std::string_view tc_id) noexcept }(); if (!opt_triple) { - return std::nullopt; + BOOST_LEAF_THROW_EXCEPTION(e_human_message{neo::ufmt("Invalid toolchain string '{}'", + tc_id)}, + BPT_ERR_REF("invalid-builtin-toolchain")); } root_map.emplace("c_compiler", opt_triple->c); @@ -295,24 +336,42 @@ std::optional toolchain::get_builtin(std::string_view tc_id) noexcept return parse_toolchain_json_data(tc_data); } -std::optional dds::toolchain::get_default() { +bpt::toolchain bpt::toolchain::get_default() { + using namespace std::literals; + auto dirs = {fs::current_path(), bpt_config_dir(), user_home_dir()}; + auto extensions = {".yaml", ".jsonc", ".json5", ".json"}; auto candidates = { fs::current_path() / "toolchain.json5", fs::current_path() / "toolchain.jsonc", fs::current_path() / "toolchain.json", - dds_config_dir() / "toolchain.json5", - dds_config_dir() / "toolchain.jsonc", - dds_config_dir() / "toolchain.json", + bpt_config_dir() / "toolchain.json5", + bpt_config_dir() / "toolchain.jsonc", + bpt_config_dir() / "toolchain.json", user_home_dir() / "toolchain.json5", user_home_dir() / "toolchain.jsonc", user_home_dir() / "toolchain.json", }; - for (auto&& cand : candidates) { - dds_log(trace, "Checking for default toolchain at [{}]", cand.string()); - if (fs::exists(cand)) { - dds_log(debug, "Using default toolchain file: {}", cand.string()); - return parse_toolchain_json5(slurp_file(cand)); + for (auto&& [ext, dir] : ranges::view::cartesian_product(extensions, dirs)) { + fs::path cand = dir / ("toolchain"s + ext); + bpt_log(trace, "Checking for default toolchain at [{}]", cand.string()); + if (not fs::is_regular_file(cand)) { + continue; } + bpt_log(debug, "Using default toolchain file: {}", cand.string()); + return toolchain::from_file(cand); } - return std::nullopt; + BOOST_LEAF_THROW_EXCEPTION(e_human_message{neo::ufmt("No default toolchain")}, + BPT_ERR_REF("no-default-toolchain"), + e_error_marker{"no-default-toolchain"}); } + +toolchain toolchain::from_file(path_ref fpath) { + if (fpath.extension().string() == ".yaml") { + auto node = bpt::parse_yaml_file(fpath); + auto data = bpt::yaml_as_json5_data(node); + return parse_toolchain_json_data(data, + neo::ufmt("Loading toolchain from [{}]", fpath.string())); + } else { + return parse_toolchain_json5(bpt::read_file(fpath)); + } +} \ No newline at end of file diff --git a/src/dds/toolchain/toolchain.hpp b/src/bpt/toolchain/toolchain.hpp similarity index 84% rename from src/dds/toolchain/toolchain.hpp rename to src/bpt/toolchain/toolchain.hpp index 8534d9c5..bca3b937 100644 --- a/src/dds/toolchain/toolchain.hpp +++ b/src/bpt/toolchain/toolchain.hpp @@ -1,14 +1,14 @@ #pragma once -#include -#include +#include +#include #include #include #include #include -namespace dds { +namespace bpt { enum class language { automatic, @@ -31,6 +31,7 @@ struct compile_file_spec { std::vector external_include_dirs = {}; language lang = language::automatic; bool enable_warnings = false; + bool syntax_only = false; }; struct compile_command_info { @@ -62,6 +63,9 @@ class toolchain { string_seq _link_exe; string_seq _warning_flags; string_seq _tty_flags; + string_seq _c_source_type_flags; + string_seq _cxx_source_type_flags; + string_seq _syntax_only_flags; std::string _archive_prefix; std::string _archive_suffix; @@ -72,6 +76,8 @@ class toolchain { enum file_deps_mode _deps_mode; + std::uint64_t _hash = 0; + public: toolchain() = default; @@ -96,8 +102,11 @@ class toolchain { path_ref cwd, toolchain_knobs) const noexcept; - static std::optional get_builtin(std::string_view key) noexcept; - static std::optional get_default(); + [[nodiscard]] std::uint64_t hash() const noexcept { return _hash; } + + static toolchain get_builtin(std::string_view key); + static toolchain get_default(); + static toolchain from_file(path_ref fpath); }; -} // namespace dds +} // namespace bpt diff --git a/src/bpt/toolchain/toolchain.test.cpp b/src/bpt/toolchain/toolchain.test.cpp new file mode 100644 index 00000000..c1af3763 --- /dev/null +++ b/src/bpt/toolchain/toolchain.test.cpp @@ -0,0 +1,19 @@ +#include +#include + +#include +#include + +#include + +TEST_CASE("Builtin toolchains reject multiple standards") { + bpt_leaf_try { + bpt::toolchain::get_builtin("c++11:c++14:gcc"); + FAIL_CHECK("Did not throw"); + } + bpt_leaf_catch(bpt::e_doc_ref ref, bpt::e_builtin_toolchain_str given) { + CHECK(ref.value == "err/invalid-builtin-toolchain.html"); + CHECK(given.value == "c++11:c++14:gcc"); + } + bpt_leaf_catch_all { FAIL_CHECK("Unknown error:" << diagnostic_info); }; +} diff --git a/src/bpt/usage_reqs.cpp b/src/bpt/usage_reqs.cpp new file mode 100644 index 00000000..db95b348 --- /dev/null +++ b/src/bpt/usage_reqs.cpp @@ -0,0 +1,198 @@ +#include "./usage_reqs.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +using namespace bpt; + +const lm::library* usage_requirement_map::get(const lm::usage& key) const noexcept { + auto found = _reqs.find(key); + if (found == _reqs.end()) { + return nullptr; + } + return &found->second; +} + +lm::library& usage_requirement_map::add(lm::usage ident) { + auto pair = std::pair(ident, lm::library{}); + auto [inserted, did_insert] = _reqs.try_emplace(ident, lm::library()); + if (!did_insert) { + BOOST_LEAF_THROW_EXCEPTION(e_dup_library_id{ident}, BPT_ERR_REF("dup-lib-name")); + } + return inserted->second; +} + +usage_requirement_map usage_requirement_map::from_lm_index(const lm::index& idx) noexcept { + usage_requirement_map ret; + for (const auto& pkg : idx.packages) { + for (const auto& lib : pkg.libraries) { + ret.add(lm::usage{pkg.name, lib.name}, lib); + } + } + return ret; +} + +std::vector usage_requirement_map::link_paths(const lm::usage& key) const { + auto req = get(key); + if (!req) { + BOOST_LEAF_THROW_EXCEPTION(e_nonesuch_library{key}, BPT_ERR_REF("unknown-usage")); + } + std::vector ret; + if (req->linkable_path) { + ret.push_back(*req->linkable_path); + } + for (const auto& dep : req->uses) { + extend(ret, link_paths(dep)); + } + for (const auto& link : req->links) { + extend(ret, link_paths(link)); + } + return ret; +} + +std::vector usage_requirement_map::include_paths(const lm::usage& usage) const { + std::vector ret; + auto lib = get(usage); + if (!lib) { + BOOST_LEAF_THROW_EXCEPTION(e_nonesuch_library{usage}, BPT_ERR_REF("unknown-usage")); + } + extend(ret, lib->include_paths); + for (const auto& transitive : lib->uses) { + extend(ret, include_paths(transitive)); + } + return ret; +} + +namespace { +// The DFS visited status for a vertex +enum class vertex_status { + unvisited, // Never seen before + visiting, // Currently looking at its children; helps find cycles + visited, // Finished examining +}; + +struct vertex_info { + vertex_status status = vertex_status::unvisited; + // When `visiting`, points to the next vertex we are searching. + // Can be chased to recover the cycle if we find a cycle. + lm::usage next; +}; + +// Implements a recursive DFS. +// Returns a lm::usage in a cycle if it exists. +// The `vertex_info->next`s can be examined to recover the cycle. +template +std::optional find_cycle(const usage_requirement_map& reqs, + std::map& vertices, + const lm::usage& usage, + const lm::library& lib) { + neo_assert_audit(invariant, + vertices.find(usage) != vertices.end(), + "Improperly initialized `vertices`; missing vertex for usage.", + usage.namespace_, + usage.name); + + vertex_info& info = vertices.find(usage)->second; + if (info.status == vertex_status::visited) { + // We've already seen this vertex before, so there's no cycle down this path + return std::nullopt; + } else if (info.status == vertex_status::visiting) { + // Found a cycle! + return usage; + } + + // Continue searching + info.status = vertex_status::visiting; + for (const lm::usage& next : lib.uses) { + info.next = next; + const lm::library* next_lib = reqs.get(next); + + if (!next_lib) { + // This is a missing library. This will be an error at a later point, but for now we + // simply ignore it. + continue; + } + + // Recursively search each child vertex. + if (auto cycle = find_cycle(reqs, vertices, next, *next_lib)) { + return cycle; + } + } + info.status = vertex_status::visited; + + // We visited everything under this vertex and didn't find a cycle, so there's no cycle down + // this path. + return std::nullopt; +} + +} // namespace + +std::optional> usage_requirement_map::find_usage_cycle() const { + // Performs the setup of the DFS and hands off to ::find_cycle() to search particular vertices. + + std::map vertices; + // Default construct each. + for (const auto& [usage, lib] : _reqs) { + vertices[usage]; + } + + // DFS from each vertex, as we don't have a source vertex. + // Reuse the same DFS state, so we still visit each vertex only once. + for (const auto& [usage, lib] : _reqs) { + if (auto cyclic_usage = find_cycle(*this, vertices, usage, lib)) { + // Follow `->next`s to recover the cycle. + std::vector cycle; + lm::usage cur = *cyclic_usage; + + do { + cycle.push_back(cur); + cur = vertices.find(cur)->second.next; + } while (cur != *cyclic_usage); + + return cycle; + } + } + + return std::nullopt; +} + +void usage_requirements::verify_acyclic() const { + // Log information on the graph to make it easier to debug issues with the DFS + bpt_log(debug, "Searching for `use` cycles."); + if (log::level_enabled(log::level::debug)) { + for (auto const& [lib, deps] : get_usage_map()) { + const auto uses_str = fmt::format("{}", fmt::join(deps.uses, ", ")); + bpt_log(debug, " lib {} uses {}", lib, uses_str); + } + } + + std::optional> cycle = get_usage_map().find_usage_cycle(); + if (cycle) { + neo_assert(invariant, + cycle->size() >= 1, + "Cycles must have at least one usage.", + cycle->size()); + + // For error formatting purposes: "a uses b uses a" instead of just "a uses b" + cycle->push_back(cycle->front()); + + write_error_marker("library-json-cyclic-dependency"); + BOOST_LEAF_THROW_EXCEPTION(make_user_error< + errc::cyclic_usage>("Cyclic dependency found: {}", + fmt::join(*cycle, " uses ")), + *cycle, + BPT_ERR_REF("cyclic-usage")); + } +} diff --git a/src/bpt/usage_reqs.hpp b/src/bpt/usage_reqs.hpp new file mode 100644 index 00000000..7436711f --- /dev/null +++ b/src/bpt/usage_reqs.hpp @@ -0,0 +1,87 @@ +#pragma once + +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace bpt { + +class shared_compile_file_rules; + +struct e_nonesuch_library { + lm::usage value; +}; + +struct e_dup_library_id { + lm::usage value; +}; + +struct e_cyclic_using { + std::vector value; +}; + +// The underlying map used by the usage requirements +class usage_requirement_map { + + using library_key = lm::usage; + + using _reqs_map_type = std::map; + _reqs_map_type _reqs; + +public: + using const_iterator = _reqs_map_type::const_iterator; + + const lm::library* get(const lm::usage& key) const noexcept; + lm::library& add(lm::usage u); + void add(lm::usage u, lm::library lib) { add(u) = lib; } + + std::vector link_paths(const lm::usage&) const; + std::vector include_paths(const lm::usage& req) const; + + static usage_requirement_map from_lm_index(const lm::index&) noexcept; + + const_iterator begin() const { return _reqs.begin(); } + const_iterator end() const { return _reqs.end(); } + + // Returns one of the cycles in the usage dependency graph, if it exists. + std::optional> find_usage_cycle() const; +}; + +// The actual usage requirements +class usage_requirements { + usage_requirement_map _reqs; + + void verify_acyclic() const; + +public: + explicit usage_requirements(usage_requirement_map reqs) + : _reqs(std::move(reqs)) { + verify_acyclic(); + } + + const lm::library* get(const lm::usage& key) const noexcept { return _reqs.get(key); } + const lm::library* get(std::string ns, std::string name) const noexcept { + return get({ns, name}); + } + + const usage_requirement_map& get_usage_map() const& { return _reqs; } + usage_requirement_map&& steal_usage_map() && { return std::move(_reqs); } + + std::vector link_paths(const lm::usage& key) const { return _reqs.link_paths(key); } + std::vector include_paths(const lm::usage& req) const { + return _reqs.include_paths(req); + } + + static usage_requirements from_lm_index(const lm::index& index) noexcept { + return usage_requirements(usage_requirement_map::from_lm_index(index)); + } +}; + +} // namespace bpt diff --git a/src/bpt/usage_reqs.test.cpp b/src/bpt/usage_reqs.test.cpp new file mode 100644 index 00000000..357a93f9 --- /dev/null +++ b/src/bpt/usage_reqs.test.cpp @@ -0,0 +1,117 @@ +#include "./usage_reqs.hpp" + +#include +#include +#include + +#include + +#include + +using Catch::Matchers::Contains; + +namespace { +class IsRotation : public Catch::MatcherBase> { + std::vector cycle; + +public: + IsRotation(std::vector cycle) + : cycle(std::move(cycle)) {} + + bool match(const std::vector& other) const override { + if (cycle.size() != other.size()) + return false; + if (cycle.empty()) + return true; + + std::vector other_cpy(other); + + auto cycle_start = std::find(other_cpy.begin(), other_cpy.end(), cycle.front()); + std::rotate(other_cpy.begin(), cycle_start, other_cpy.end()); + + return std::equal(cycle.begin(), cycle.end(), other_cpy.begin()); + } + + std::string describe() const override { + return fmt::format("is a rotation of: {}", fmt::join(cycle, ", ")); + } +}; +} // namespace + +TEST_CASE("Cyclic dependencies are rejected") { + bpt::usage_requirement_map reqs; + { + std::vector previous; + for (int i = 0; i < 10; ++i) { + std::string dep_name = fmt::format("dep{}", i + 10); + + reqs.add({dep_name, dep_name}, lm::library{.name = dep_name, .uses = previous}); + + previous.push_back(lm::usage{dep_name, dep_name}); + } + } + + reqs.add({"dep1", "dep1"}, lm::library{.name = "dep1", .uses = {lm::usage{"dep2", "dep2"}}}); + reqs.add({"dep2", "dep2"}, lm::library{.name = "dep2", .uses = {lm::usage{"dep3", "dep3"}}}); + reqs.add({"dep3", "dep3"}, lm::library{.name = "dep3", .uses = {lm::usage{"dep1", "dep1"}}}); + + REQUIRE_THROWS_WITH(bpt::usage_requirements(reqs), + Contains("'dep1/dep1' uses 'dep2/dep2' uses 'dep3/dep3' uses 'dep1/dep1'")); +} + +TEST_CASE("Self-referential `uses` are rejected") { + bpt::usage_requirement_map reqs; + + reqs.add({"dep1", "dep1"}, lm::library{.name = "dep1", .uses = {lm::usage{"dep1", "dep1"}}}); + + REQUIRE_THROWS_WITH(bpt::usage_requirements(reqs), Contains("'dep1/dep1' uses 'dep1/dep1'")); +} + +TEST_CASE("Cyclic dependencies can be found - Self-referential") { + bpt::usage_requirement_map reqs; + reqs.add({"dep1", "dep1"}, lm::library{.name = "dep1", .uses = {lm::usage{"dep1", "dep1"}}}); + + auto cycle = reqs.find_usage_cycle(); + REQUIRE(cycle.has_value()); + CHECK_THAT(*cycle, IsRotation({lm::usage{"dep1", "dep1"}})); +} + +TEST_CASE("Cyclic dependencies can be found - Longer loops") { + bpt::usage_requirement_map reqs; + reqs.add({"dep1", "dep1"}, lm::library{.name = "dep1", .uses = {lm::usage{"dep2", "dep2"}}}); + reqs.add({"dep2", "dep2"}, lm::library{.name = "dep2", .uses = {lm::usage{"dep3", "dep3"}}}); + reqs.add({"dep3", "dep3"}, lm::library{.name = "dep3", .uses = {lm::usage{"dep1", "dep1"}}}); + + auto cycle = reqs.find_usage_cycle(); + REQUIRE(cycle.has_value()); + CHECK_THAT(*cycle, + IsRotation({lm::usage{"dep1", "dep1"}, + lm::usage{"dep2", "dep2"}, + lm::usage{"dep3", "dep3"}})); +} + +TEST_CASE("Cyclic dependencies can be found - larger case") { + bpt::usage_requirement_map reqs; + + { + std::vector previous; + for (int i = 0; i < 10; ++i) { + std::string dep_name = fmt::format("dep{}", i + 10); + + reqs.add({dep_name, dep_name}, lm::library{.name = dep_name, .uses = previous}); + + previous.push_back(lm::usage{dep_name, dep_name}); + } + } + + reqs.add({"dep1", "dep1"}, lm::library{.name = "dep1", .uses = {lm::usage{"dep2", "dep2"}}}); + reqs.add({"dep2", "dep2"}, lm::library{.name = "dep2", .uses = {lm::usage{"dep3", "dep3"}}}); + reqs.add({"dep3", "dep3"}, lm::library{.name = "dep3", .uses = {lm::usage{"dep1", "dep1"}}}); + + auto cycle = reqs.find_usage_cycle(); + REQUIRE(cycle.has_value()); + CHECK_THAT(*cycle, + IsRotation({lm::usage{"dep1", "dep1"}, + lm::usage{"dep2", "dep2"}, + lm::usage{"dep3", "dep3"}})); +} \ No newline at end of file diff --git a/src/dds/util/algo.hpp b/src/bpt/util/algo.hpp similarity index 80% rename from src/dds/util/algo.hpp rename to src/bpt/util/algo.hpp index 422d8143..8d625252 100644 --- a/src/dds/util/algo.hpp +++ b/src/bpt/util/algo.hpp @@ -5,7 +5,7 @@ #include #include -namespace dds { +namespace bpt { template void erase_if(Container& c, Predicate&& p) { @@ -21,9 +21,16 @@ void extend(Container& c, Iter iter, const Stop stop) { } template -void extend(Container& c, Iter iter, Iter end) { +void extend(Container& c, Iter iter, Iter end) requires requires { c.insert(c.end(), iter, end); } +{ c.insert(c.end(), iter, end); } + +template +void extend(Container& c, Iter iter, Iter end) requires requires { + c.insert(iter, end); +} +{ c.insert(iter, end); } template void extend(Container& c, Other&& o) { @@ -49,4 +56,4 @@ void sort_unique_erase(Container& c) noexcept { template using ref_vector = std::vector>; -} // namespace dds +} // namespace bpt diff --git a/src/bpt/util/compress.cpp b/src/bpt/util/compress.cpp new file mode 100644 index 00000000..10a3b6cc --- /dev/null +++ b/src/bpt/util/compress.cpp @@ -0,0 +1,67 @@ +#include "./compress.hpp" + +#include +#include +#include + +#include +#include +#include + +using namespace bpt; + +result bpt::compress_file_gz(fs::path in_path, fs::path out_path) noexcept { + BPT_E_SCOPE(e_read_file_path{in_path}); + BPT_E_SCOPE(e_write_file_path{out_path}); + std::error_code ec; + + auto in_file = neo::file_stream::open(in_path, neo::open_mode::read, ec); + if (ec) { + return boost::leaf::new_error(ec, + e_compress_error{"Failed to open input file for reading"}); + } + + auto out_file = neo::file_stream::open(out_path, neo::open_mode::write, ec); + if (ec) { + return boost::leaf::new_error(ec, + e_compress_error{"Failed to open output file for writing"}); + } + + try { + neo::gzip_compress(neo::stream_io_buffers{*out_file}, neo::stream_io_buffers{*in_file}); + } catch (const std::system_error& e) { + return boost::leaf::new_error(e.code(), e_compress_error{e.what()}); + } catch (const std::runtime_error& e) { + return boost::leaf::new_error(e_compress_error{e.what()}); + } + + return {}; +} + +result bpt::decompress_file_gz(fs::path gz_path, fs::path plain_path) noexcept { + BPT_E_SCOPE(e_read_file_path{gz_path}); + BPT_E_SCOPE(e_write_file_path{plain_path}); + + std::error_code ec; + auto in_file = neo::file_stream::open(gz_path, neo::open_mode::read, ec); + if (ec) { + return boost::leaf::new_error(ec, + e_decompress_error{"Failed to open input file for reading"}); + } + + auto out_file = neo::file_stream::open(plain_path, neo::open_mode::write, ec); + if (ec) { + return boost::leaf::new_error(ec, + e_decompress_error{"Failed to open output file for writing"}); + } + + try { + neo::gzip_decompress(neo::stream_io_buffers{*out_file}, neo::stream_io_buffers{*in_file}); + } catch (const std::system_error& e) { + return boost::leaf::new_error(e.code(), e_decompress_error{e.what()}); + } catch (const std::runtime_error& e) { + return boost::leaf::new_error(e_decompress_error{e.what()}); + } + + return {}; +} \ No newline at end of file diff --git a/src/bpt/util/compress.hpp b/src/bpt/util/compress.hpp new file mode 100644 index 00000000..b41a82ad --- /dev/null +++ b/src/bpt/util/compress.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "./fs/path.hpp" +#include + +namespace bpt { + +struct e_compress_error { + std::string value; +}; +[[nodiscard]] result compress_file_gz(fs::path in_file, fs::path out_file) noexcept; + +struct e_decompress_error { + std::string value; +}; +[[nodiscard]] result decompress_file_gz(fs::path in_file, fs::path out_file) noexcept; + +} // namespace bpt diff --git a/src/bpt/util/compress.test.cpp b/src/bpt/util/compress.test.cpp new file mode 100644 index 00000000..095a4583 --- /dev/null +++ b/src/bpt/util/compress.test.cpp @@ -0,0 +1,73 @@ +#include +#include +#include +#include + +#include +#include + +template +void check_voidres(Fun&& f) { + boost::leaf::try_handle_all(f, [](const boost::leaf::verbose_diagnostic_info& info) { + FAIL(info); + }); +} + +TEST_CASE("Compress/uncompress a file") { + const std::string_view plain_string = "I am the text content"; + auto tdir = bpt::temporary_dir::create(); + bpt::fs::create_directories(tdir.path()); + const auto test_file = tdir.path() / "test.txt"; + bpt::write_file(test_file, plain_string); + CHECK(bpt::read_file(test_file) == plain_string); + auto test_file_gz = bpt::fs::path(test_file) += ".gz"; + check_voidres([&] { return bpt::compress_file_gz(test_file, test_file_gz); }); + + auto compressed_content = bpt::read_file(test_file_gz); + CHECK_FALSE(compressed_content.empty()); + CHECK(compressed_content != plain_string); + + auto decomp_file = bpt::fs::path(test_file) += ".plain"; + check_voidres([&] { return bpt::decompress_file_gz(test_file_gz, decomp_file); }); + CHECK(bpt::read_file(decomp_file) == plain_string); +} + +TEST_CASE("Fail to compress a non-existent file") { + auto tdir = bpt::temporary_dir::create(); + bpt::fs::create_directories(tdir.path()); + boost::leaf::try_handle_all( + [&]() -> bpt::result { + BOOST_LEAF_CHECK(bpt::compress_file_gz(tdir.path() / "noexist.txt", + tdir.path() / "doesn't matter.gz")); + FAIL("Compression should have failed"); + return {}; + }, + [&](bpt::e_compress_error, + std::error_code ec, + bpt::e_read_file_path infile, + bpt::e_write_file_path outfile) { + CHECK(ec == std::errc::no_such_file_or_directory); + CHECK(infile.value == (tdir.path() / "noexist.txt")); + CHECK(outfile.value == (tdir.path() / "doesn't matter.gz")); + }, + [](const boost::leaf::verbose_diagnostic_info& info) { + FAIL("Unexpected failure: " << info); + }); +} + +TEST_CASE("Decompress a non-compressed file") { + auto tdir = bpt::temporary_dir::create(); + bpt::fs::create_directories(tdir.path()); + boost::leaf::try_handle_all( + [&]() -> bpt::result { + BOOST_LEAF_CHECK(bpt::decompress_file_gz(__FILE__, tdir.path() / "dummy.txt")); + FAIL("Decompression should have failed"); + return {}; + }, + [&](bpt::e_decompress_error, bpt::e_read_file_path infile) { + CHECK(infile.value == __FILE__); + }, + [](const boost::leaf::verbose_diagnostic_info& info) { + FAIL("Unexpected failure: " << info); + }); +} \ No newline at end of file diff --git a/src/bpt/util/copy.hpp b/src/bpt/util/copy.hpp new file mode 100644 index 00000000..7d8615d4 --- /dev/null +++ b/src/bpt/util/copy.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +#include +#include + +namespace bpt { + +template +constexpr std::decay_t +decay_copy(T&& arg) noexcept requires std::constructible_from, T &&> { + return static_cast>(NEO_FWD(arg)); +} + +} // namespace bpt diff --git a/src/bpt/util/db/db.cpp b/src/bpt/util/db/db.cpp new file mode 100644 index 00000000..71553ad8 --- /dev/null +++ b/src/bpt/util/db/db.cpp @@ -0,0 +1,80 @@ +#include "./db.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +using namespace bpt; +namespace nsql = neo::sqlite3; + +struct unique_database::impl { + nsql::connection db; + nsql::statement_cache cache{db}; +}; + +unique_database::unique_database(std::unique_ptr iptr) noexcept + : _impl(std::move(iptr)) {} + +unique_database::~unique_database() noexcept = default; +unique_database::unique_database(unique_database&&) noexcept = default; +unique_database& unique_database::operator=(unique_database&&) noexcept = default; + +static result _open_db(neo::zstring_view str, nsql::openmode mode) noexcept { + BPT_E_SCOPE(e_db_open_path{std::string(str)}); + auto db = nsql::connection::open(str, mode); + if (!db.has_value()) { + auto ec = nsql::make_error_code(db.errc()); + bpt_log(debug, "Error opening SQLite database [{}]: {}", str, ec.message()); + return new_error(db.error(), e_db_open_ec{ec}); + } + auto e = db->exec(R"( + PRAGMA foreign_keys = 1; + PRAGMA vdbe_trace = 1; + PRAGMA busy_timeout = 60000; + )"); + if (e.is_error()) { + auto ec = nsql::make_error_code(e.errc()); + bpt_log(debug, "Error initializing SQLite database [{}]: {}", str, ec.message()); + return new_error(e.error(), e_db_open_ec{ec}); + } + bpt_log(trace, "Successfully opened SQLite database [{}]", str); + return std::move(*db); +} + +result unique_database::open(neo::zstring_view str) noexcept { + bpt_log(debug, "Opening/creating SQLite database [{}]", str); + BOOST_LEAF_AUTO(db, _open_db(str, nsql::openmode::readwrite | nsql::openmode::create)); + return unique_database{std::make_unique(std::move(db))}; +} + +result unique_database::open_existing(neo::zstring_view str) noexcept { + bpt_log(debug, "Opening existing SQLite database [{}]", str); + BOOST_LEAF_AUTO(db, _open_db(str, nsql::openmode::readwrite)); + return unique_database{std::make_unique(std::move(db))}; +} + +void unique_database::exec_script(nsql::sql_string_literal script) { + _impl->db.exec(neo::zstring_view(script.string())).throw_if_error(); +} + +nsql::connection_ref unique_database::sqlite3_db() const noexcept { return _impl->db; } + +nsql::statement& unique_database::prepare(nsql::sql_string_literal s) const { + try { + return _impl->cache(s); + } catch (const neo::sqlite3::error& exc) { + auto err = new_error(e_sqlite3_error{exc.db_message()}, exc.code()); + if (exc.code().category() == neo::sqlite3::error_category()) { + err.load(neo::sqlite3::errc{exc.code().value()}); + } + throw; + } +} diff --git a/src/bpt/util/db/db.hpp b/src/bpt/util/db/db.hpp new file mode 100644 index 00000000..ce497f7a --- /dev/null +++ b/src/bpt/util/db/db.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include + +#include +#include +#include + +#include + +namespace bpt { + +struct e_db_open_path { + std::string value; +}; + +struct e_db_open_ec { + std::error_code value; +}; + +struct e_sqlite3_error { + std::string value; +}; + +class unique_database { + struct impl; + + std::unique_ptr _impl; + + explicit unique_database(std::unique_ptr db) noexcept; + +public: + [[nodiscard]] static result open(neo::zstring_view str) noexcept; + [[nodiscard]] static result open_existing(neo::zstring_view str) noexcept; + + ~unique_database(); + unique_database(unique_database&&) noexcept; + unique_database& operator=(unique_database&&) noexcept; + + neo::sqlite3::connection_ref sqlite3_db() const noexcept; + neo::sqlite3::statement& prepare(neo::sqlite3::sql_string_literal) const; + + void exec_script(neo::sqlite3::sql_string_literal); +}; + +} // namespace bpt diff --git a/src/bpt/util/db/db.test.cpp b/src/bpt/util/db/db.test.cpp new file mode 100644 index 00000000..2adb2321 --- /dev/null +++ b/src/bpt/util/db/db.test.cpp @@ -0,0 +1,20 @@ +#include "./db.hpp" + +#include + +#include + +using namespace neo::sqlite3::literals; + +TEST_CASE("Open a database") { + auto db = bpt::unique_database::open(":memory:"); + CHECK(db); + db->exec_script(R"( + CREATE TABLE foo (bar TEXT, baz INTEGER); + INSERT INTO foo VALUES ('quux', 42); + )"_sql); + db->exec_script(R"( + INSERT INTO foo + SELECT * FROM foo + )"_sql); +} diff --git a/src/bpt/util/db/migrate.cpp b/src/bpt/util/db/migrate.cpp new file mode 100644 index 00000000..06c2899c --- /dev/null +++ b/src/bpt/util/db/migrate.cpp @@ -0,0 +1,114 @@ +#include "./migrate.hpp" + +#include "./db.hpp" + +#include + +#include +#include +#include +#include +#include + +using namespace bpt; +namespace nsql = neo::sqlite3; + +result detail::do_migrations_1(unique_database& db, + std::string_view tablename, + std::initializer_list migrations) { + auto init_meta_table = fmt::format( + R"( + CREATE TABLE IF NOT EXISTS "{}" AS + WITH init (version) AS (VALUES (0)) + SELECT * FROM init + )", + tablename); + auto st = db.sqlite3_db().prepare(init_meta_table); + if (!st.has_value()) { + return new_error(db_migration_errc::init_failed, + st.errc(), + e_migration_error{fmt::format( + "Failed to prepare initialize-meta-table statement for '{}': {}: {}", + tablename, + nsql::make_error_code(st.errc()).message(), + db.sqlite3_db().error_message())}); + } + auto step_rc = st->step(); + if (step_rc != neo::sqlite3::errc::done) { + return new_error(db_migration_errc::init_failed, + step_rc.errc(), + e_migration_error{ + fmt::format("Failed to initialize migration meta-table '{}': {}: {}", + tablename, + nsql::make_error_code(st.errc()).message(), + db.sqlite3_db().error_message())}); + } + + // Check the migration version + BOOST_LEAF_AUTO(version, get_migration_version(db, tablename)); + if (version < 0) { + return new_error(db_migration_errc::invalid_version_number, + e_migration_error{"Database migration value is negative"}); + } + if (version > static_cast(migrations.size())) { + return new_error(db_migration_errc::too_new, + e_migration_error{"Database migration is too new"}); + } + + // Wrap migrations in a transaction + neo::sqlite3::transaction_guard tr{db.sqlite3_db()}; + + // Apply individual migrations + auto it = migrations.begin() + version; + for (; it != migrations.end(); ++it) { + try { + (*it)->apply(db); + } catch (const neo::sqlite3::error& err) { + tr.rollback(); + return new_error(db_migration_errc::generic_error, + e_migration_error{std::string(err.what())}); + } + } + + // Update the version in the meta table + std::string query = fmt::format("UPDATE \"{}\" SET version = {}", tablename, migrations.size()); + st = *db.sqlite3_db().prepare(query); + step_rc = st->step(); + if (step_rc != neo::sqlite3::errc::done) { + tr.rollback(); + return new_error(db_migration_errc::generic_error, + e_migration_error{ + fmt::format("Failed to update migration version on '{}': {}: {}", + tablename, + nsql::make_error_code(step_rc.errc()).message(), + db.sqlite3_db().error_message())}); + } + + return {}; +} + +result bpt::get_migration_version(unique_database& db, std::string_view tablename) { + auto q = fmt::format("SELECT version FROM \"{}\"", tablename); + auto st = db.sqlite3_db().prepare(q); + if (!st.has_value()) { + return new_error(e_migration_error{ + fmt::format("Failed to find version for migrations table '{}': {}: {}", + tablename, + nsql::make_error_code(st.errc()).message(), + db.sqlite3_db().error_message())}); + } + auto step_rc = st->step(); + if (step_rc != neo::sqlite3::errc::row) { + return new_error("Failed to find version for migrations table '{}': {}: {}", + tablename, + nsql::make_error_code(step_rc.errc()).message(), + db.sqlite3_db().error_message()); + } + auto r = st->row()[0]; + if (!r.is_integer()) { + return new_error(e_migration_error{ + fmt::format("Invalid 'version' in meta table '{}': Value must be an integer", + tablename)}); + } + return static_cast(r.as_integer()); +} diff --git a/src/bpt/util/db/migrate.hpp b/src/bpt/util/db/migrate.hpp new file mode 100644 index 00000000..9e46cf1a --- /dev/null +++ b/src/bpt/util/db/migrate.hpp @@ -0,0 +1,71 @@ +#pragma once + +#include +#include +#include + +#include + +namespace bpt { + +class unique_database; + +enum class db_migration_errc { + init_failed, + invalid_version_number, + too_new, + generic_error, +}; + +/// Error value returned when a migration fails +struct e_migration_error { + std::string value; +}; + +namespace detail { + +/// type-erasure for migration functions +struct erased_migration { + virtual void apply(unique_database&) = 0; +}; + +/// Impl for type-erased migration functions +template +struct migration_fn : erased_migration { + Func& fn; + + explicit migration_fn(Func& f) + : fn(f) {} + + void apply(unique_database& db) override { fn(db); } +}; + +/// Do type-erased migrations +result do_migrations_1(unique_database& db, + std::string_view tablename, + std::initializer_list migrations); + +/// type-erasing ship +template +result do_migrations(unique_database& db, std::string_view tablename, Func&&... migrations) { + /// Convert the given migrations to pointers-to-base: + std::initializer_list migrations_il = {&migrations...}; + /// Run those migrations + return do_migrations_1(db, tablename, migrations_il); +} + +} // namespace detail + +/// Get the migration meta version from the database stored in the given table name +result get_migration_version(unique_database& db, std::string_view tablename); + +/** + * @brief Execute database migrations on the database, and update the meta table "tablename" + */ +template +[[nodiscard]] result +apply_db_migrations(unique_database& db, std::string_view tablename, Func&&... fn) { + return detail::do_migrations(db, tablename, detail::migration_fn{fn}...); +} + +} // namespace bpt diff --git a/src/bpt/util/db/migrate.test.cpp b/src/bpt/util/db/migrate.test.cpp new file mode 100644 index 00000000..484a2f75 --- /dev/null +++ b/src/bpt/util/db/migrate.test.cpp @@ -0,0 +1,31 @@ +#include "./migrate.hpp" + +#include "./db.hpp" + +#include + +using namespace neo::sqlite3::literals; + +struct empty_database { + bpt::unique_database db = std::move(bpt::unique_database::open(":memory:").value()); +}; + +TEST_CASE_METHOD(empty_database, "Run some simple migrations") { + bpt::apply_db_migrations( // + db, + "test_meta", + [](auto& db) { + db.exec_script(R"( + CREATE TABLE foo (bar TEXT); + CREATE TABLE baz (quux INTEGER); + )"_sql); + }) + .value(); + auto version = bpt::get_migration_version(db, "test_meta"); + REQUIRE(version); + CHECK(*version == 1); + db.exec_script(R"( + INSERT INTO foo VALUES ('I am a string'); + INSERT INTO baz VALUES (42); + )"_sql); +} diff --git a/src/bpt/util/db/query.hpp b/src/bpt/util/db/query.hpp new file mode 100644 index 00000000..7836cfe1 --- /dev/null +++ b/src/bpt/util/db/query.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include + +#include +#include +#include + +#include "./db.hpp" + +namespace bpt { + +template +[[nodiscard]] result db_bind(neo::sqlite3::statement& st, const Args&... args) noexcept { + auto rc = st.bindings().bind_all(args...); + if (rc.is_error()) { + return new_error(rc.errc(), make_error_code(rc.errc())); + } + return {}; +} + +/** + * @brief Execute a database query and return a lazy tuple iterator for the given Ts... + */ +template +[[nodiscard]] auto db_query(neo::sqlite3::statement& st, const Args&... args) { + neo_assert(expects, + !st.is_busy(), + "db_query<>() started on a statement that is already executing. Someone forgot to " + "call .reset()"); + st.reset(); + st.bindings().bind_all(args...).throw_if_error(); + return neo::sqlite3::iter_tuples{st}; +} + +/** + * @brief Obtain a single row from the database using the given query. + */ +template +[[nodiscard]] result> db_single(neo::sqlite3::statement_mutref st, + const Args&... args) { + st->reset(); + BOOST_LEAF_CHECK(db_bind(st, args...)); + auto rc = st->step(); + if (rc != neo::sqlite3::errc::row) { + return new_error(rc.errc(), make_error_code(rc.errc())); + } + auto tup = st->row().template unpack().as_tuple(); + st->reset(); + return tup; +} + +/** + * @brief Obtain a single value from the database of the given type T + */ +template +[[nodiscard]] result db_cell(neo::sqlite3::statement_mutref st, const Args&... args) { + BOOST_LEAF_AUTO(row, db_single(st, args...)); + auto [value] = NEO_FWD(row); + return value; +} + +/** + * @brief Execute the given SQL statement until completion. + */ +template +[[nodiscard]] result db_exec(neo::sqlite3::statement_mutref st, const Args&... args) { + st->reset(); + BOOST_LEAF_CHECK(db_bind(st, args...)); + auto rst = st->auto_reset(); + auto rc = st->run_to_completion(); + if (rc.is_error()) { + return new_error(rc.errc(), make_error_code(rc.errc())); + } + return {}; +} + +} // namespace bpt diff --git a/src/bpt/util/db/query.test.cpp b/src/bpt/util/db/query.test.cpp new file mode 100644 index 00000000..80fd42d3 --- /dev/null +++ b/src/bpt/util/db/query.test.cpp @@ -0,0 +1,32 @@ +#include "./query.hpp" + +#include + +#include + +using namespace neo::sqlite3::literals; + +TEST_CASE("Database operations") { + auto db = bpt::unique_database::open(":memory:"); + CHECK(db); + db->exec_script(R"( + CREATE TABLE foo (bar TEXT, baz INTEGER); + INSERT INTO foo VALUES ('quux', 42); + )"_sql); + + auto& get_row = db->prepare("SELECT * FROM foo"_sql); + auto tups = bpt::db_query(get_row); + auto [str, v] = *tups.begin(); + CHECK(str == "quux"); + CHECK(v == 42); + get_row.reset(); + + auto [str2, v2] = bpt::db_single(get_row).value(); + CHECK(str2 == "quux"); + CHECK(v2 == 42); + get_row.reset(); + + auto single = bpt::db_cell(db->prepare("SELECT bar FROM foo LIMIT 1"_sql)); + REQUIRE(single); + CHECK(*single == "quux"); +} diff --git a/src/dds/util/env.cpp b/src/bpt/util/env.cpp similarity index 57% rename from src/dds/util/env.cpp rename to src/bpt/util/env.cpp index 97bc13d7..07e7c4b6 100644 --- a/src/dds/util/env.cpp +++ b/src/bpt/util/env.cpp @@ -4,7 +4,7 @@ #include -std::optional dds::getenv(const std::string& varname) noexcept { +std::optional bpt::getenv(const std::string& varname) noexcept { auto cptr = std::getenv(varname.data()); if (cptr) { return std::string(cptr); @@ -12,7 +12,11 @@ std::optional dds::getenv(const std::string& varname) noexcept { return {}; } -bool dds::getenv_bool(const std::string& varname) noexcept { +bool bpt::getenv_bool(const std::string& varname) noexcept { auto s = getenv(varname); + return s.has_value() && is_truthy_string(*s); +} + +bool bpt::is_truthy_string(std::string_view s) noexcept { return s == neo::oper::any_of("1", "true", "on", "TRUE", "ON", "YES", "yes"); } diff --git a/src/dds/util/env.hpp b/src/bpt/util/env.hpp similarity index 82% rename from src/dds/util/env.hpp rename to src/bpt/util/env.hpp index 37d16314..659d70c1 100644 --- a/src/dds/util/env.hpp +++ b/src/bpt/util/env.hpp @@ -5,12 +5,14 @@ #include #include -namespace dds { +namespace bpt { std::optional getenv(const std::string& env) noexcept; bool getenv_bool(const std::string& env) noexcept; +bool is_truthy_string(std::string_view s) noexcept; + template std::string getenv(const std::string& name, Func&& fn) noexcept(noexcept(fn())) { auto val = getenv(name); @@ -20,4 +22,4 @@ std::string getenv(const std::string& name, Func&& fn) noexcept(noexcept(fn())) return *val; } -} // namespace dds +} // namespace bpt diff --git a/src/dds/util/flock.hpp b/src/bpt/util/flock.hpp similarity index 52% rename from src/dds/util/flock.hpp rename to src/bpt/util/flock.hpp index 17c66247..e6157e12 100644 --- a/src/dds/util/flock.hpp +++ b/src/bpt/util/flock.hpp @@ -1,21 +1,21 @@ #pragma once -#include +#include -namespace dds { +namespace bpt { class shared_file_mutex { - fs::path _path; - void* _lock_data = nullptr; + std::filesystem::path _path; + void* _lock_data = nullptr; public: - shared_file_mutex(path_ref p); + shared_file_mutex(const std::filesystem::path& p); shared_file_mutex(const shared_file_mutex&) = delete; ~shared_file_mutex(); - path_ref path() const noexcept { return _path; } + const std::filesystem::path& path() const noexcept { return _path; } bool try_lock() noexcept; bool try_lock_shared() noexcept; @@ -25,4 +25,4 @@ class shared_file_mutex { void unlock_shared(); }; -} // namespace dds +} // namespace bpt diff --git a/src/dds/util/flock.nix.cpp b/src/bpt/util/flock.nix.cpp similarity index 96% rename from src/dds/util/flock.nix.cpp rename to src/bpt/util/flock.nix.cpp index 52a5b6cd..1a37a43c 100644 --- a/src/dds/util/flock.nix.cpp +++ b/src/bpt/util/flock.nix.cpp @@ -1,8 +1,10 @@ -#ifndef _WIN32 - #include "./flock.hpp" -#include +#include + +#ifndef _WIN32 + +#include #include @@ -12,7 +14,7 @@ #include -using namespace dds; +using namespace bpt; namespace { diff --git a/src/dds/util/flock.win.cpp b/src/bpt/util/flock.win.cpp similarity index 97% rename from src/dds/util/flock.win.cpp rename to src/bpt/util/flock.win.cpp index ae23af42..26522ed5 100644 --- a/src/dds/util/flock.win.cpp +++ b/src/bpt/util/flock.win.cpp @@ -1,15 +1,17 @@ -#ifdef _WIN32 - #include "./flock.hpp" -#include +#include + +#ifdef _WIN32 + +#include #include #include #include -using namespace dds; +using namespace bpt; namespace { diff --git a/src/dds/util/fnmatch.cpp b/src/bpt/util/fnmatch.cpp similarity index 94% rename from src/dds/util/fnmatch.cpp rename to src/bpt/util/fnmatch.cpp index 9eb37464..11c5ff37 100644 --- a/src/dds/util/fnmatch.cpp +++ b/src/bpt/util/fnmatch.cpp @@ -7,7 +7,7 @@ using charptr = const char*; -namespace dds::detail::fnmatch { +namespace bpt::detail::fnmatch { namespace { @@ -107,7 +107,7 @@ class pattern_impl { if (c == '\\') { ++cur; if (cur == last) { - throw std::runtime_error("Untermated [group] in pattern"); + throw std::runtime_error("Unterminated [group] in pattern"); } chars.push_back(*cur); } else { @@ -177,13 +177,13 @@ class pattern_impl { } }; -} // namespace dds::detail::fnmatch +} // namespace bpt::detail::fnmatch -dds::fnmatch::pattern dds::fnmatch::compile(std::string_view str) { +bpt::fnmatch::pattern bpt::fnmatch::compile(std::string_view str) { return pattern{std::make_shared(str)}; } -bool dds::fnmatch::pattern::_match(charptr first, charptr last) const noexcept { +bool bpt::fnmatch::pattern::_match(charptr first, charptr last) const noexcept { assert(_impl); return _impl->match(first, last); } diff --git a/src/dds/util/fnmatch.hpp b/src/bpt/util/fnmatch.hpp similarity index 98% rename from src/dds/util/fnmatch.hpp rename to src/bpt/util/fnmatch.hpp index 702087cf..d6020036 100644 --- a/src/dds/util/fnmatch.hpp +++ b/src/bpt/util/fnmatch.hpp @@ -7,7 +7,7 @@ #include #include -namespace dds { +namespace bpt { namespace fnmatch { @@ -96,7 +96,7 @@ constexpr auto compile_next(String s) { constexpr auto str = s(); constexpr auto cur_char = str[Cur]; if constexpr (Cur == Len) { - return dds::fnmatch::ct_pattern(); + return bpt::fnmatch::ct_pattern(); } else if constexpr (cur_char == '*') { return compile_next(s); } else if constexpr (cur_char == '?') { @@ -303,7 +303,7 @@ class pattern { } bool match(const char* str) const { - return match(str, str + dds::detail::fnmatch::length(str)); + return match(str, str + bpt::detail::fnmatch::length(str)); } template @@ -318,4 +318,4 @@ class pattern { } // namespace fnmatch -} // namespace dds +} // namespace bpt diff --git a/src/dds/util/fnmatch.test.cpp b/src/bpt/util/fnmatch.test.cpp similarity index 81% rename from src/dds/util/fnmatch.test.cpp rename to src/bpt/util/fnmatch.test.cpp index 9c1f2677..5df99c34 100644 --- a/src/dds/util/fnmatch.test.cpp +++ b/src/bpt/util/fnmatch.test.cpp @@ -1,9 +1,9 @@ -#include +#include #include TEST_CASE("Basic fnmatch matching") { - auto pat = dds::fnmatch::compile("foo.bar"); + auto pat = bpt::fnmatch::compile("foo.bar"); CHECK_FALSE(pat.match("foo.baz")); CHECK_FALSE(pat.match("foo.")); CHECK_FALSE(pat.match("foo.barz")); @@ -11,7 +11,7 @@ TEST_CASE("Basic fnmatch matching") { CHECK_FALSE(pat.match(" foo.bar")); CHECK(pat.match("foo.bar")); - pat = dds::fnmatch::compile("foo.*"); + pat = bpt::fnmatch::compile("foo.*"); CHECK(pat.match("foo.")); auto m = pat.match("foo.b"); CHECK(m); @@ -19,7 +19,7 @@ TEST_CASE("Basic fnmatch matching") { CHECK_FALSE(pat.match("foo")); CHECK_FALSE(pat.match(" foo.bar")); - pat = dds::fnmatch::compile("foo.*.cpp"); + pat = bpt::fnmatch::compile("foo.*.cpp"); for (auto fname : {"foo.bar.cpp", "foo..cpp", "foo.cat.cpp"}) { auto m = pat.match(fname); CHECK(m); diff --git a/src/bpt/util/fs/dirscan.cpp b/src/bpt/util/fs/dirscan.cpp new file mode 100644 index 00000000..86c2205b --- /dev/null +++ b/src/bpt/util/fs/dirscan.cpp @@ -0,0 +1,98 @@ +#include "./dirscan.hpp" + +#include +#include + +#include +#include +#include +#include + +using namespace bpt; +using namespace neo::sqlite3::literals; + +file_collector file_collector::create(unique_database& db) { + apply_db_migrations( // + db, + "bpt_source_collector_meta", + [](unique_database& db) { // + db.exec_script(R"( + CREATE TABLE bpt_scanned_dirs ( + dir_id INTEGER PRIMARY KEY, + dirpath TEXT NOT NULL UNIQUE + ); + CREATE TABLE bpt_found_files ( + file_id INTEGER PRIMARY KEY, + dir_id INTEGER + NOT NULL + REFERENCES bpt_scanned_dirs + ON DELETE CASCADE, + relpath TEXT NOT NULL, + UNIQUE (dir_id, relpath) + ); + )"_sql); + }) + .value(); + return file_collector{db}; +} + +neo::any_input_range file_collector::collect(path_ref dirpath) { + std::error_code ec; + // Normalize the path so that different paths pointing to the same dir will hit caches + auto normpath = fs::weakly_canonical(dirpath, ec); + // Try and select the row corresponding to this directory + std::int64_t dir_id = -1; + auto dir_id_ = neo::sqlite3::one_row( // + _db.get().prepare("SELECT dir_id FROM bpt_scanned_dirs WHERE dirpath = ?"_sql), + normpath.string()); + + if (dir_id_.has_value()) { + dir_id = std::get<0>(*dir_id_); + } else { + // Nothing for this directory. We need to scan it + neo::sqlite3::transaction_guard tr{_db.get().sqlite3_db()}; + + auto new_dir_id = *neo::sqlite3::one_row( // + _db.get().prepare(R"( + INSERT INTO bpt_scanned_dirs (dirpath) + VALUES (?) + RETURNING dir_id + )"_sql), + normpath.string()); + auto dir_iter = fs::recursive_directory_iterator{normpath}; + auto children = dir_iter | std::views::transform([&](fs::path p) { + return std::tuple(std::get<0>(new_dir_id), + p.lexically_relative(normpath).string()); + }); + neo::sqlite3::exec_each( // + _db.get().prepare(R"( + INSERT INTO bpt_found_files (dir_id, relpath) + VALUES (?, ?) + )"_sql), + children) + .throw_if_error(); + dir_id = std::get<0>(new_dir_id); + } + + auto st = neo::copy_shared( + *_db.get().sqlite3_db().prepare("SELECT relpath FROM bpt_found_files WHERE dir_id = ?")); + + return *neo::sqlite3::exec_tuples(*st, dir_id) + | std::views::transform([pin = st](auto tup) { return fs::path(tup.template get<0>()); }); +} + +bool file_collector::has_cached(path_ref dirpath) noexcept { + auto normpath = fs::weakly_canonical(dirpath); + auto has_dir = // + db_cell(_db.get().prepare( + "VALUES (EXISTS (SELECT * FROM bpt_scanned_dirs WHERE dirpath = ?))"_sql), + std::string_view(normpath.string())); + return has_dir && *has_dir; +} + +void file_collector::forget(path_ref dirpath) noexcept { + auto normpath = fs::weakly_canonical(dirpath); + auto res = db_exec(_db.get().prepare("DELETE FROM bpt_scanned_dirs WHERE dirpath = ?"_sql), + std::string_view(normpath.string())); + assert(res); +} diff --git a/src/bpt/util/fs/dirscan.hpp b/src/bpt/util/fs/dirscan.hpp new file mode 100644 index 00000000..45a6d2f2 --- /dev/null +++ b/src/bpt/util/fs/dirscan.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include + +#include + +#include +#include + +namespace bpt { + +class file_collector { + std::reference_wrapper _db; + + explicit file_collector(unique_database& db) noexcept + : _db(db) {} + +public: + // Create a new collector with the given database as the cache source + [[nodiscard]] static file_collector create(unique_database& db); + + // Obtain a recursive listing of every descendant of the given directory + neo::any_input_range collect(path_ref); + // Remove the given directory from the database cache + void forget(path_ref) noexcept; + /// Determine whether the collector has a cache entry for the given directory + [[nodiscard]] bool has_cached(path_ref) noexcept; +}; + +} // namespace bpt diff --git a/src/bpt/util/fs/dirscan.test.cpp b/src/bpt/util/fs/dirscan.test.cpp new file mode 100644 index 00000000..e3333350 --- /dev/null +++ b/src/bpt/util/fs/dirscan.test.cpp @@ -0,0 +1,20 @@ +#include "./dirscan.hpp" + +#include + +#include + +#include + +TEST_CASE("Create a simple scanner") { + auto this_dir = bpt::fs::path(__FILE__).lexically_normal().parent_path(); + auto db = bpt::unique_database::open(":memory:"); + REQUIRE(db); + auto finder = bpt::file_collector::create(*db); + CHECK_FALSE(finder.has_cached(this_dir)); + auto found = finder.collect(this_dir) | neo::to_vector; + CHECK_FALSE(found.empty()); + CHECK(finder.has_cached(this_dir)); + finder.forget(this_dir); + CHECK_FALSE(finder.has_cached(this_dir)); +} diff --git a/src/bpt/util/fs/io.cpp b/src/bpt/util/fs/io.cpp new file mode 100644 index 00000000..d3b93641 --- /dev/null +++ b/src/bpt/util/fs/io.cpp @@ -0,0 +1,53 @@ +#include "./io.hpp" + +#include + +#include +#include +#include + +#include + +using namespace bpt; + +using path_ref = const std::filesystem::path&; + +std::fstream bpt::open_file(path_ref fpath, std::ios::openmode mode) { + BPT_E_SCOPE(e_open_file_path{fpath}); + errno = 0; + std::fstream ret{fpath, mode}; + auto e = errno; + if (!ret) { + auto ec = std::error_code{e, std::system_category()}; + BOOST_LEAF_THROW_EXCEPTION(std::system_error(ec, + neo::ufmt("Failed to open file [{}]", + fpath.string())), + boost::leaf::e_errno{e}, + ec); + } + return ret; +} + +void bpt::write_file(path_ref dest, std::string_view content) { + BPT_E_SCOPE(e_write_file_path{dest}); + auto ofile = open_file(dest, std::ios::binary | std::ios::out); + errno = 0; + ofile.write(content.data(), content.size()); + auto e = errno; + if (!ofile) { + auto ec = std::error_code(e, std::system_category()); + BOOST_LEAF_THROW_EXCEPTION(std::system_error(ec, + neo::ufmt("Failed to write to file [{}]", + dest.string())), + boost::leaf::e_errno{e}, + ec); + } +} + +std::string bpt::read_file(path_ref path) { + BPT_E_SCOPE(e_read_file_path{path}); + auto infile = open_file(path, std::ios::binary | std::ios::in); + std::ostringstream out; + out << infile.rdbuf(); + return std::move(out).str(); +} diff --git a/src/bpt/util/fs/io.hpp b/src/bpt/util/fs/io.hpp new file mode 100644 index 00000000..13b7cd3c --- /dev/null +++ b/src/bpt/util/fs/io.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include +#include +#include + +namespace bpt { + +struct e_open_file_path { + std::filesystem::path value; +}; + +struct e_write_file_path { + std::filesystem::path value; +}; + +struct e_read_file_path { + std::filesystem::path value; +}; + +[[nodiscard]] std::fstream open_file(std::filesystem::path const& filepath, std::ios::openmode); +void write_file(std::filesystem::path const& path, std::string_view); +[[nodiscard]] std::string read_file(std::filesystem::path const& path); + +} // namespace bpt diff --git a/src/bpt/util/fs/op.cpp b/src/bpt/util/fs/op.cpp new file mode 100644 index 00000000..66214e42 --- /dev/null +++ b/src/bpt/util/fs/op.cpp @@ -0,0 +1,22 @@ +#include "./op.hpp" + +#include + +#include +#include + +using namespace bpt; + +bool bpt::file_exists(const std::filesystem::path& filepath) { + std::error_code ec; + auto r = std::filesystem::exists(filepath, ec); + if (ec) { + BOOST_LEAF_THROW_EXCEPTION( + std::system_error{ec, + neo::ufmt("Error checking for the existence of a file [{}]", + filepath.string())}, + filepath, + ec); + } + return r; +} diff --git a/src/bpt/util/fs/op.hpp b/src/bpt/util/fs/op.hpp new file mode 100644 index 00000000..4ccceab0 --- /dev/null +++ b/src/bpt/util/fs/op.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace bpt { + +bool file_exists(const std::filesystem::path&); + +} // namespace bpt diff --git a/src/bpt/util/fs/path.cpp b/src/bpt/util/fs/path.cpp new file mode 100644 index 00000000..548e899b --- /dev/null +++ b/src/bpt/util/fs/path.cpp @@ -0,0 +1,26 @@ +#include "./path.hpp" + +#include + +using namespace bpt; + +fs::path bpt::normalize_path(path_ref p_) noexcept { + auto p = p_.lexically_normal(); + while (!p.empty() && p.filename().empty()) { + p = p.parent_path(); + } + return p; +} + +fs::path bpt::resolve_path_weak(path_ref p) noexcept { + return normalize_path(fs::weakly_canonical(p)); +} + +result bpt::resolve_path_strong(path_ref p_) noexcept { + std::error_code ec; + auto p = fs::canonical(p_, ec); + if (ec) { + return boost::leaf::new_error(ec, p_, e_resolve_path{p_}); + } + return normalize_path(p); +} \ No newline at end of file diff --git a/src/bpt/util/fs/path.hpp b/src/bpt/util/fs/path.hpp new file mode 100644 index 00000000..72ed64b6 --- /dev/null +++ b/src/bpt/util/fs/path.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include + +#include + +namespace bpt { + +namespace fs = std::filesystem; + +/** + * @brief Alias of a const& to a std::filesystem::path + */ +using path_ref = const fs::path&; + +/** + * @brief An error occurring when resolving a filepath + */ +struct e_resolve_path { + fs::path value; +}; + +/** + * @brief Convert a path to its most-normal form based on bpt's path-equivalence. + * + * @param p A path to be normalized. + * @return fs::path The normal form of the path + * + * Two paths are "equivalent" (in bpt) if they would resolve to the same entity on disk from a given + * base location. This removes redundant path elements (dots and dot-dots), removes trailing + * directory separators, and converts the path to it's POSIX (generic) form. + */ +[[nodiscard]] fs::path normalize_path(path_ref p) noexcept; + +/** + * @brief Obtain the normalized absolute path to a possibly-existing file or directory. + */ +[[nodiscard]] fs::path resolve_path_weak(path_ref p) noexcept; + +/** + * @brief Obtain the normalized path to an existing file or directory. + */ +[[nodiscard]] result resolve_path_strong(path_ref p) noexcept; + +} // namespace bpt diff --git a/src/bpt/util/fs/path.test.cpp b/src/bpt/util/fs/path.test.cpp new file mode 100644 index 00000000..511e4d2c --- /dev/null +++ b/src/bpt/util/fs/path.test.cpp @@ -0,0 +1,22 @@ +#include "./path.hpp" + +#include + +#include + +TEST_CASE("Normalize some paths") { + CHECK(bpt::normalize_path("foo").string() == "foo"); + if constexpr (neo::os_is_windows) { + CHECK(bpt::normalize_path("foo/bar").string() == "foo\\bar"); + CHECK(bpt::normalize_path("foo/bar/").string() == "foo\\bar"); + CHECK(bpt::normalize_path("foo//bar/").string() == "foo\\bar"); + CHECK(bpt::normalize_path("foo/./bar/").string() == "foo\\bar"); + CHECK(bpt::normalize_path("foo/../foo/bar/").string() == "foo\\bar"); + } else { + CHECK(bpt::normalize_path("foo/bar").string() == "foo/bar"); + CHECK(bpt::normalize_path("foo/bar/").string() == "foo/bar"); + CHECK(bpt::normalize_path("foo//bar/").string() == "foo/bar"); + CHECK(bpt::normalize_path("foo/./bar/").string() == "foo/bar"); + CHECK(bpt::normalize_path("foo/../foo/bar/").string() == "foo/bar"); + } +} diff --git a/src/bpt/util/fs/shutil.cpp b/src/bpt/util/fs/shutil.cpp new file mode 100644 index 00000000..b37ce4d1 --- /dev/null +++ b/src/bpt/util/fs/shutil.cpp @@ -0,0 +1,133 @@ +#include "./shutil.hpp" + +#include "./path.hpp" + +#include +#include +#include +#include +#include + +#include + +#include + +using namespace bpt; + +result bpt::ensure_absent(path_ref path) noexcept { + BPT_E_SCOPE(e_remove_file{path}); + std::error_code ec; + bpt_log(trace, "Recursive ensure-absent [{}]", path.string()); + fs::remove_all(path, ec); + if (ec) { + const bool is_enoent = ec == std::errc::no_such_file_or_directory; + bpt_log(trace, + " Ensure-absent error while removing [{}]: {}{}", + path.string(), + ec.message(), + is_enoent ? " (Ignoring this error)" : ""); + if (not is_enoent) { + return new_error(ec); + } + } + return {}; +} + +result bpt::remove_file(path_ref path) noexcept { + BPT_E_SCOPE(e_remove_file{path}); + std::error_code ec; + fs::remove(path, ec); + if (ec) { + return new_error(ec); + } + return {}; +} + +result bpt::move_file(path_ref source, path_ref dest) { + std::error_code ec; + BPT_E_SCOPE(e_move_file{source, dest}); + BPT_E_SCOPE(ec); + + fs::rename(source, dest, ec); + if (!ec) { + return {}; + } + + if (ec != std::errc::cross_device_link && ec != std::errc::permission_denied) { + return BOOST_LEAF_NEW_ERROR(); + } + + auto tmp = bpt_leaf_try_some { return bpt::temporary_dir::create_in(dest.parent_path()); } + bpt_leaf_catch(const std::system_error& exc) { return BOOST_LEAF_NEW_ERROR(exc, exc.code()); }; + BOOST_LEAF_CHECK(tmp); + + fs::copy(source, tmp->path(), fs::copy_options::recursive, ec); + if (ec) { + return BOOST_LEAF_NEW_ERROR(); + } + fs::rename(tmp->path(), dest, ec); + if (ec) { + return BOOST_LEAF_NEW_ERROR(); + } + fs::remove_all(source, ec); + // Drop 'ec' + return {}; +} + +result bpt::copy_file(path_ref source, path_ref dest, fs::copy_options opts) noexcept { + std::error_code ec; + BPT_E_SCOPE(e_copy_file{source, dest}); + BPT_E_SCOPE(ec); + opts &= ~fs::copy_options::recursive; + fs::copy_file(source, dest, opts, ec); + if (ec) { + return BOOST_LEAF_NEW_ERROR(); + } + return {}; +} + +result bpt::create_symlink(path_ref target, path_ref symlink) noexcept { + std::error_code ec; + BPT_E_SCOPE(e_symlink{symlink, target}); + BPT_E_SCOPE(ec); + /// XXX: 'target' might not refer to an existing file, or might be a relative path from the + /// symlink dest dir. + if (fs::is_directory(target)) { + fs::create_directory_symlink(target, symlink, ec); + } else { + fs::create_symlink(target, symlink, ec); + } + if (ec) { + return BOOST_LEAF_NEW_ERROR(); + } + return {}; +} + +static result copy_tree_1(path_ref source, path_ref dest, fs::copy_options opts) noexcept { + if (!fs::is_directory(source)) { + return BOOST_LEAF_NEW_ERROR(std::make_error_code(std::errc::not_a_directory)); + } + if (!fs::is_directory(dest)) { + std::error_code ec; + fs::create_directories(dest, ec); + if (ec) { + return BOOST_LEAF_NEW_ERROR(ec); + } + } + for (fs::directory_entry entry : fs::directory_iterator{source}) { + auto subdest = dest / entry.path().filename(); + if (entry.is_directory()) { + BOOST_LEAF_CHECK(copy_tree_1(entry.path(), subdest, opts)); + } else { + BOOST_LEAF_CHECK(bpt::copy_file(entry.path(), subdest, opts)); + } + } + return {}; +} + +result bpt::copy_tree(path_ref source, path_ref dest, fs::copy_options opts) noexcept { + BPT_E_SCOPE(e_copy_tree{source, dest}); + BOOST_LEAF_AUTO(src, resolve_path_strong(source)); + auto dst = resolve_path_weak(dest); + return copy_tree_1(src, dst, opts); +} diff --git a/src/bpt/util/fs/shutil.hpp b/src/bpt/util/fs/shutil.hpp new file mode 100644 index 00000000..85323190 --- /dev/null +++ b/src/bpt/util/fs/shutil.hpp @@ -0,0 +1,101 @@ +#pragma once + +#include "./path.hpp" + +#include + +#include + +namespace bpt { + +struct e_remove_file { + fs::path value; +}; + +/** + * @brief Ensure that the named file/directory does not exist. + * + * If the file does not exist, no error occurs. + */ +[[nodiscard]] result ensure_absent(path_ref path) noexcept; + +/** + * @brief Delete the named file/directory. + * + * If the file does not exist, results in an error. + */ +[[nodiscard]] result remove_file(path_ref file) noexcept; + +/** + * @brief Move the file or directory 'source' to 'dest' + * + * @param source The file or directory to move + * @param dest The destination path (not parent directory!) of the file/directory + */ +[[nodiscard]] result move_file(path_ref source, path_ref dest); + +struct e_copy_file { + fs::path source; + fs::path dest; + + friend std::ostream& operator<<(std::ostream& out, const e_copy_file& self) noexcept { + out << "e_copy_file: From [" << self.source.string() << "] to [" << self.dest.string() + << "]"; + return out; + } +}; + +struct e_move_file { + fs::path source; + fs::path dest; + + friend std::ostream& operator<<(std::ostream& out, const e_move_file& self) noexcept { + out << "e_move_file: From [" << self.source.string() << "] to [" << self.dest.string() + << "]"; + return out; + } +}; + +struct e_symlink { + fs::path symlink; + fs::path target; + + friend std::ostream& operator<<(std::ostream& out, const e_symlink& self) noexcept { + out << "e_symlink: Symlink [" << self.symlink.string() << "] -> [" << self.target.string() + << "]"; + return out; + } +}; + +/** + * @brief Copy the given file or directory to 'dest' + * + * @param source The file to copy + * @param dest The destination of the copied file (not the parent directory!) + * @param opts Options for the copy operation + */ +[[nodiscard]] result +copy_file(path_ref source, path_ref dest, fs::copy_options opts = {}) noexcept; + +struct e_copy_tree { + fs::path source; + fs::path dest; + + friend std::ostream& operator<<(std::ostream& out, const e_copy_tree& self) noexcept { + out << "e_symlink: From [" << self.source.string() << "] to [" << self.dest.string() << "]"; + return out; + } +}; + +[[nodiscard]] result +copy_tree(path_ref source, path_ref dest, fs::copy_options opts = {}) noexcept; + +/** + * @brief Create a symbolic link at 'symlink' that points to 'target' + * + * @param target The target of the symlink + * @param symlink The path to the symlink object + */ +[[nodiscard]] result create_symlink(path_ref target, path_ref symlink) noexcept; + +} // namespace bpt diff --git a/src/dds/util/glob.cpp b/src/bpt/util/glob.cpp similarity index 90% rename from src/dds/util/glob.cpp rename to src/bpt/util/glob.cpp index 342cb9c7..8dae0aff 100644 --- a/src/dds/util/glob.cpp +++ b/src/bpt/util/glob.cpp @@ -16,10 +16,10 @@ enum glob_coro_ret { } // namespace -namespace dds::detail { +namespace bpt::detail { struct rglob_item { - std::optional pattern; + std::optional pattern; }; struct glob_impl { @@ -134,14 +134,14 @@ struct glob_iter_state { CONTINUE(); } -}; // namespace dds::detail +}; // namespace bpt::detail -} // namespace dds::detail +} // namespace bpt::detail namespace { -dds::detail::glob_impl compile_glob_expr(std::string_view pattern) { - using namespace dds::detail; +bpt::detail::glob_impl compile_glob_expr(std::string_view pattern) { + using namespace bpt::detail; glob_impl acc{}; acc.spelling = std::string(pattern); @@ -158,7 +158,7 @@ dds::detail::glob_impl compile_glob_expr(std::string_view pattern) { if (next_part == "**") { acc.items.emplace_back(); } else { - acc.items.push_back({dds::fnmatch::compile(next_part)}); + acc.items.push_back({bpt::fnmatch::compile(next_part)}); } } @@ -171,7 +171,7 @@ dds::detail::glob_impl compile_glob_expr(std::string_view pattern) { } // namespace -dds::glob_iterator::glob_iterator(dds::glob gl, dds::path_ref root) +bpt::glob_iterator::glob_iterator(bpt::glob gl, bpt::path_ref root) : _impl(gl._impl) , _done(false) { @@ -179,7 +179,7 @@ dds::glob_iterator::glob_iterator(dds::glob gl, dds::path_ref root) increment(); } -void dds::glob_iterator::increment() { +void bpt::glob_iterator::increment() { auto st = reenter_again; while (st == reenter_again) { st = _state->reenter(); @@ -187,20 +187,20 @@ void dds::glob_iterator::increment() { _done = st == done; } -dds::fs::directory_entry dds::glob_iterator::dereference() const noexcept { +bpt::fs::directory_entry bpt::glob_iterator::dereference() const noexcept { return _state->get_entry(); } -dds::glob dds::glob::compile(std::string_view pattern) { +bpt::glob bpt::glob::compile(std::string_view pattern) { glob ret; - ret._impl = std::make_shared(compile_glob_expr(pattern)); + ret._impl = std::make_shared(compile_glob_expr(pattern)); return ret; } namespace { -using path_iter = dds::fs::path::const_iterator; -using pat_iter = std::vector::const_iterator; +using path_iter = bpt::fs::path::const_iterator; +using pat_iter = std::vector::const_iterator; bool check_matches(path_iter elem_it, const path_iter elem_stop, @@ -236,11 +236,11 @@ bool check_matches(path_iter elem_it, } // namespace -bool dds::glob::match(dds::path_ref filepath) const noexcept { +bool bpt::glob::match(bpt::path_ref filepath) const noexcept { return check_matches(filepath.begin(), filepath.end(), _impl->items.cbegin(), _impl->items.cend()); } -std::string_view dds::glob::string() const noexcept { return _impl->spelling; } +std::string_view bpt::glob::string() const noexcept { return _impl->spelling; } diff --git a/src/dds/util/glob.hpp b/src/bpt/util/glob.hpp similarity index 90% rename from src/dds/util/glob.hpp rename to src/bpt/util/glob.hpp index 9a89ef4b..7da48430 100644 --- a/src/dds/util/glob.hpp +++ b/src/bpt/util/glob.hpp @@ -1,13 +1,13 @@ #pragma once -#include +#include #include #include #include -namespace dds { +namespace bpt { namespace detail { @@ -35,6 +35,7 @@ class glob_iterator : public neo::iterator_facade { struct sentinel_type {}; + bool operator==(sentinel_type) const noexcept { return at_end(); } bool at_end() const noexcept { return _done; } glob_iterator begin() const noexcept { return *this; } @@ -60,4 +61,4 @@ class glob { std::string_view string() const noexcept; }; -} // namespace dds +} // namespace bpt diff --git a/src/dds/util/glob.test.cpp b/src/bpt/util/glob.test.cpp similarity index 77% rename from src/dds/util/glob.test.cpp rename to src/bpt/util/glob.test.cpp index e815e4f3..1d062b3f 100644 --- a/src/dds/util/glob.test.cpp +++ b/src/bpt/util/glob.test.cpp @@ -1,10 +1,10 @@ -#include +#include #include TEST_CASE("Simple glob") { - auto this_dir = dds::fs::path(__FILE__).parent_path(); - auto glob = dds::glob::compile("*.test.cpp"); + auto this_dir = bpt::fs::path(__FILE__).parent_path(); + auto glob = bpt::glob::compile("*.test.cpp"); ::setlocale(LC_ALL, ".utf8"); auto it = glob.scan_from(this_dir); @@ -19,17 +19,17 @@ TEST_CASE("Simple glob") { CHECK(n_found > 0); n_found = 0; - for (auto found : dds::glob::compile("glob.test.cpp").scan_from(this_dir)) { + for (auto found : bpt::glob::compile("glob.test.cpp").scan_from(this_dir)) { n_found++; } CHECK(n_found == 1); - auto me_it = dds::glob::compile("src/**/glob.test.cpp").begin(); + auto me_it = bpt::glob::compile("src/**/glob.test.cpp").begin(); REQUIRE(!me_it.at_end()); ++me_it; CHECK(me_it.at_end()); - auto all_tests = dds::glob::compile("src/**/*.test.cpp"); + auto all_tests = bpt::glob::compile("src/**/*.test.cpp"); n_found = 0; for (auto f : all_tests) { n_found += 1; @@ -39,7 +39,7 @@ TEST_CASE("Simple glob") { } TEST_CASE("Check globs") { - auto glob = dds::glob::compile("foo/bar*/baz"); + auto glob = bpt::glob::compile("foo/bar*/baz"); CHECK(glob.match("foo/bar/baz")); CHECK(glob.match("foo/barffff/baz")); CHECK_FALSE(glob.match("foo/bar")); @@ -47,7 +47,7 @@ TEST_CASE("Check globs") { CHECK_FALSE(glob.match("foo/bar/bazf")); CHECK_FALSE(glob.match("foo/bar/")); - glob = dds::glob::compile("foo/**/bar.txt"); + glob = bpt::glob::compile("foo/**/bar.txt"); CHECK(glob.match("foo/bar.txt")); CHECK(glob.match("foo/thing/bar.txt")); CHECK(glob.match("foo/thing/another/bar.txt")); @@ -60,7 +60,7 @@ TEST_CASE("Check globs") { CHECK_FALSE(glob.match("foo/thing/bar.txt/fail")); CHECK_FALSE(glob.match("foo/bar.txt/fail")); - glob = dds::glob::compile("foo/**/bar/**/baz.txt"); + glob = bpt::glob::compile("foo/**/bar/**/baz.txt"); CHECK(glob.match("foo/bar/baz.txt")); CHECK(glob.match("foo/thing/bar/baz.txt")); CHECK(glob.match("foo/thing/bar/baz.txt")); @@ -68,6 +68,6 @@ TEST_CASE("Check globs") { CHECK(glob.match("foo/bar/thing/baz.txt")); CHECK(glob.match("foo/bar/baz/baz.txt")); - glob = dds::glob::compile("doc/**"); + glob = bpt::glob::compile("doc/**"); CHECK(glob.match("doc/something.txt")); } diff --git a/src/bpt/util/http/error.hpp b/src/bpt/util/http/error.hpp new file mode 100644 index 00000000..11ed7ffe --- /dev/null +++ b/src/bpt/util/http/error.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +namespace bpt { + +struct http_error : std::runtime_error { +private: + int _resp_code; + +public: + using runtime_error::runtime_error; + + explicit http_error(int status, std::string message) + : runtime_error(message) + , _resp_code(status) {} + + int status_code() const noexcept { return _resp_code; } +}; + +struct http_status_error : http_error { + using http_error::http_error; +}; + +struct http_server_error : http_error { + using http_error::http_error; +}; + +struct e_http_request_url { + std::string value; +}; + +enum class e_http_status : int {}; + +} // namespace bpt diff --git a/src/dds/util/http/pool.cpp b/src/bpt/util/http/pool.cpp similarity index 74% rename from src/dds/util/http/pool.cpp rename to src/bpt/util/http/pool.cpp index f6f73aea..86e76496 100644 --- a/src/dds/util/http/pool.cpp +++ b/src/bpt/util/http/pool.cpp @@ -1,8 +1,10 @@ #include "./pool.hpp" -#include -#include -#include +#include "./error.hpp" + +#include +#include +#include #include #include @@ -12,11 +14,12 @@ #include #include #include +#include #include #include -namespace dds::detail { +namespace bpt::detail { struct http_client_impl { network_origin origin; @@ -55,7 +58,7 @@ struct http_client_impl { } void connect() { - DDS_E_SCOPE(origin); + BPT_E_SCOPE(origin); auto addr = neo::address::resolve(origin.hostname, std::to_string(origin.port)); auto sock = neo::socket::open_connected(addr, neo::socket::type::stream); @@ -97,7 +100,7 @@ struct http_client_impl { .parse_tail = {}, }; - dds_log(debug, + bpt_log(debug, " --> HTTP {} {}://{}:{}{}", params.method, origin.protocol, @@ -113,7 +116,7 @@ struct http_client_impl { {"Content-Length", "0"}, {"TE", "gzip, chunked"}, {"Connection", "keep-alive"}, - {"User-Agent", "dds 0.1.0-alpha.6"}, + {"User-Agent", "bpt 0.1.0-alpha.6"}, }; if (!params.prior_etag.empty()) { headers.push_back({"If-None-Match", params.prior_etag}); @@ -146,7 +149,7 @@ struct http_client_impl { } bool disconnect = false; if (r.version == neo::http::version::v1_0) { - dds_log(trace, "HTTP/1.0 server will disconnect by default"); + bpt_log(trace, " HTTP/1.0 server will disconnect by default"); disconnect = true; } else if (r.version == neo::http::version::v1_1) { disconnect = r.header_value("Connection") == "close"; @@ -155,7 +158,7 @@ struct http_client_impl { disconnect = true; } _peer_disconnected = disconnect; - dds_log(debug, " <-- HTTP {} {}", r.status, r.status_message); + bpt_log(debug, " <-- HTTP {} {}", r.status, r.status_message); return r; } }; @@ -171,9 +174,9 @@ struct http_pool_impl { std::multimap, origin_order> _clients; }; -} // namespace dds::detail +} // namespace bpt::detail -using namespace dds; +using namespace bpt; using client_impl_ptr = std::shared_ptr; @@ -190,7 +193,7 @@ http_client::~http_client() { } if (_impl->_state != detail::http_client_impl::_state_t::ready && _n_exceptions != std::uncaught_exceptions()) { - dds_log(debug, "NOTE: An http_client was dropped due to an exception"); + bpt_log(debug, "NOTE: An http_client was dropped due to an exception"); return; } neo_assert(expects, @@ -230,7 +233,7 @@ http_client http_pool::client_for_origin(const network_origin& origin) { ret._pool = _impl; if (iter == _impl->_clients.end()) { // Nothing for this origin yet - dds_log(debug, + bpt_log(debug, "Opening new connection to {}://{}:{}", origin.protocol, origin.hostname, @@ -239,7 +242,7 @@ http_client http_pool::client_for_origin(const network_origin& origin) { ptr->connect(); ret._impl = ptr; } else { - dds_log(debug, + bpt_log(debug, "Reusing existing connection to {}://{}:{}", origin.protocol, origin.hostname, @@ -261,7 +264,7 @@ namespace { struct recv_none_state : erased_message_body { neo::const_buffer next(std::size_t) override { return {}; } - void consume(std::size_t) override {} + void consume(std::size_t) noexcept override {} }; template @@ -281,7 +284,7 @@ struct recv_chunked_state : erased_message_body { } return part; } - void consume(std::size_t n) override { _chunked.consume(n); } + void consume(std::size_t n) noexcept override { _chunked.consume(n); } }; template @@ -301,7 +304,7 @@ struct recv_gzip_state : erased_message_body { } return part; } - void consume(std::size_t n) override { _gzip.consume(n); } + void consume(std::size_t n) noexcept override { _gzip.consume(n); } }; template @@ -316,54 +319,82 @@ struct recv_plain_state : erased_message_body { , _client(cl) {} neo::const_buffer next(std::size_t n) override { + if (_size == 0) { + // We are done reading. Don't perform another read, just return. + _client->_state = detail::http_client_impl::_state_t::ready; + return neo::const_buffer(); + } auto part = _strm.next((std::min)(n, _size)); if (neo::buffer_is_empty(part)) { _client->_state = detail::http_client_impl::_state_t::ready; } return part; } - void consume(std::size_t n) override { + void consume(std::size_t n) noexcept override { _size -= n; return _strm.consume(n); } }; +template +struct recv_http_v1_0_state : erased_message_body { + Stream& _strm; + client_impl_ptr _client; + + explicit recv_http_v1_0_state(Stream& s, client_impl_ptr cl) + : _strm(s) + , _client(cl) {} + + neo::const_buffer next(std::size_t n) override { + auto part = _strm.next(n); + if (neo::buffer_is_empty(part)) { + _client->_state = detail::http_client_impl::_state_t::ready; + } + return part; + } + + void consume(std::size_t n) noexcept override { return _strm.consume(n); } +}; + } // namespace std::unique_ptr http_client::_make_body_reader(const http_response_info& res) { + neo_assertion_breadcrumbs("Creating a body reader for HTTP response", + int(_impl->_state), + _impl->origin.protocol, + _impl->origin.hostname, + _impl->origin.port, + res.status, + res.status_message); neo_assert( expects, _impl->_state == detail::http_client_impl::_state_t::recvd_resp_head, - "Invalid state to ready HTTP response body. Have not yet received the response header", - int(_impl->_state), - _impl->origin.protocol, - _impl->origin.hostname, - _impl->origin.port); + "Invalid state to ready HTTP response body. Have not yet received the response header"); if (res.status < 200 || res.status == 204 || res.status == 304) { return std::make_unique(); } return _impl->_do_io([&](auto&& source) -> std::unique_ptr { using source_type = decltype(source); if (res.content_length() == 0) { - dds_log(trace, "Empty response body"); + bpt_log(trace, "Empty response body"); _set_ready(); return std::make_unique(); } else if (res.transfer_encoding() == "chunked") { - dds_log(trace, "Chunked response body"); + bpt_log(trace, "Chunked response body"); return std::make_unique>(source, _impl); } else if (res.transfer_encoding() == "gzip") { - dds_log(trace, "GZip encoded response body"); + bpt_log(trace, "GZip encoded response body"); return std::make_unique>(source, _impl); } else if (!res.transfer_encoding().has_value() && res.content_length() > 0) { - dds_log(trace, "Plain response body"); + bpt_log(trace, "Plain response body"); return std::make_unique>(source, *res.content_length(), _impl); + } else if (res.version == neo::http::version::v1_0) { + bpt_log(trace, "HTTP/1.0 response body"); + return std::make_unique>(source, _impl); } else { - neo_assert(invariant, - false, - "Unimplemented", - res.transfer_encoding().value_or("[null]")); + neo_assert(invariant, false, "Unimplemented", res.transfer_encoding()); } }); } @@ -385,9 +416,16 @@ void http_client::_set_ready() noexcept { _impl->_state = detail::http_client_impl::_state_t::ready; } -request_result http_pool::request(neo::url url, http_request_params params) { +void http_client::abort_client() noexcept { + _impl->_peer_disconnected = true; + _set_ready(); +} + +request_result http_pool::request(neo::url_view url_, http_request_params params) { + auto url = url_.normalized(); + neo_assertion_breadcrumbs("Issuing HTTP request", url.to_string()); for (auto i = 0; i <= 100; ++i) { - DDS_E_SCOPE(url); + BPT_E_SCOPE(url); params.path = url.path; params.query = url.query.value_or(""); @@ -396,11 +434,11 @@ request_result http_pool::request(neo::url url, http_request_params params) { client.send_head(params); auto resp = client.recv_head(); - DDS_E_SCOPE(resp); + BPT_E_SCOPE(resp); - if (dds::log::level_enabled(dds::log::level::trace)) { + if (bpt::log::level_enabled(bpt::log::level::trace)) { for (auto hdr : resp.headers) { - dds_log(trace, " -- {}: {}", hdr.key, hdr.value); + bpt_log(trace, " -- {}: {}", hdr.key, hdr.value); } } @@ -411,22 +449,38 @@ request_result http_pool::request(neo::url url, http_request_params params) { if (resp.is_error()) { client.discard_body(resp); - throw BOOST_LEAF_EXCEPTION(http_status_error("Received an error from HTTP")); + throw BOOST_LEAF_EXCEPTION(http_status_error(resp.status, + "Received an error from HTTP"), + e_http_status{resp.status}); } if (resp.is_redirect()) { client.discard_body(resp); if (i == 100) { throw BOOST_LEAF_EXCEPTION( - http_server_error("Encountered over 100 HTTP redirects. Request aborted.")); + http_server_error(resp.status, + "Encountered over 100 HTTP redirects. Request aborted."), + e_http_status{resp.status}); } auto loc = resp.headers.find("Location"); if (!loc) { throw BOOST_LEAF_EXCEPTION( - http_server_error("Server sent an invalid response of a 30x redirect without a " - "'Location' header")); + http_server_error(resp.status, + "Server sent an invalid response of a 30x redirect without a " + "'Location' header"), + e_http_status{resp.status}); + } + bpt_log(debug, + "HTTP {} {} [{}] -> [{}]", + resp.status, + resp.status_message, + url.to_string(), + loc->value); + if (loc->value.starts_with("/")) { + url.path = std::string(loc->value); + } else { + url = bpt::parse_url(loc->value); } - url = neo::url::parse(loc->value); continue; } @@ -434,3 +488,8 @@ request_result http_pool::request(neo::url url, http_request_params params) { } neo::unreachable(); } + +void bpt::request_result::save_file(const std::filesystem::path& dest) { + auto out = neo::file_stream::open(dest, neo::open_mode::write); + client.recv_body_into(resp, neo::stream_io_buffers(out)); +} diff --git a/src/dds/util/http/pool.hpp b/src/bpt/util/http/pool.hpp similarity index 83% rename from src/dds/util/http/pool.hpp rename to src/bpt/util/http/pool.hpp index 750c2fd3..0ba23cbc 100644 --- a/src/dds/util/http/pool.hpp +++ b/src/bpt/util/http/pool.hpp @@ -3,6 +3,8 @@ #include "./request.hpp" #include "./response.hpp" +#include + #include #include #include @@ -10,9 +12,10 @@ #include #include +#include #include -namespace dds { +namespace bpt { namespace detail { @@ -24,17 +27,9 @@ struct http_client_impl; } // namespace detail struct erased_message_body { - virtual ~erased_message_body() = default; - virtual neo::const_buffer next(std::size_t n) = 0; - virtual void consume(std::size_t n) = 0; -}; - -class http_status_error : public std::runtime_error { - using runtime_error::runtime_error; -}; - -class http_server_error : public std::runtime_error { - using runtime_error::runtime_error; + virtual ~erased_message_body() = default; + virtual neo::const_buffer next(std::size_t n) = 0; + virtual void consume(std::size_t n) noexcept = 0; }; struct network_origin { @@ -99,6 +94,8 @@ class http_client { } void discard_body(const http_response_info&); + + void abort_client() noexcept; }; struct request_result { @@ -106,6 +103,8 @@ struct request_result { http_response_info resp; void discard_body() { client.discard_body(resp); } + + void save_file(std::filesystem::path const&); }; class http_pool { @@ -130,8 +129,8 @@ class http_pool { http_client client_for_origin(const network_origin&); - request_result request(neo::url url, http_request_params params); - auto request(neo::url url) { return request(url, http_request_params{}); } + request_result request(neo::url_view url, http_request_params params); + auto request(neo::url_view url) { return request(url, http_request_params{}); } }; -} // namespace dds +} // namespace bpt diff --git a/src/dds/util/http/pool.test.cpp b/src/bpt/util/http/pool.test.cpp similarity index 84% rename from src/dds/util/http/pool.test.cpp rename to src/bpt/util/http/pool.test.cpp index e8c210ce..4585f2df 100644 --- a/src/dds/util/http/pool.test.cpp +++ b/src/bpt/util/http/pool.test.cpp @@ -5,10 +5,10 @@ #include -TEST_CASE("Create an empty pool") { dds::http_pool pool; } +TEST_CASE("Create an empty pool") { bpt::http_pool pool; } TEST_CASE("Connect to a remote") { - dds::http_pool pool; + bpt::http_pool pool; // auto client = pool.access(); auto cl = pool.client_for_origin({"https", "www.google.com", 443}); cl.send_head({.method = "GET", .path = "/"}); @@ -19,7 +19,7 @@ TEST_CASE("Connect to a remote") { } TEST_CASE("Issue a request on a pool") { - dds::http_pool pool; + bpt::http_pool pool; auto resp = pool.request(neo::url::parse("https://www.google.com")); resp.discard_body(); } diff --git a/src/dds/util/http/request.hpp b/src/bpt/util/http/request.hpp similarity index 89% rename from src/dds/util/http/request.hpp rename to src/bpt/util/http/request.hpp index 3c925486..039d6254 100644 --- a/src/dds/util/http/request.hpp +++ b/src/bpt/util/http/request.hpp @@ -4,7 +4,7 @@ #include -namespace dds { +namespace bpt { struct http_request_params { std::string_view method = "GET"; @@ -17,4 +17,4 @@ struct http_request_params { std::string_view last_modified{}; }; -} // namespace dds +} // namespace bpt diff --git a/src/dds/util/http/response.cpp b/src/bpt/util/http/response.cpp similarity index 92% rename from src/dds/util/http/response.cpp rename to src/bpt/util/http/response.cpp index 273bc7b3..b70dc3b6 100644 --- a/src/dds/util/http/response.cpp +++ b/src/bpt/util/http/response.cpp @@ -1,12 +1,12 @@ #include "./response.hpp" -#include +#include #include #include -using namespace dds; +using namespace bpt; std::optional http_response_info::content_length() const noexcept { auto cl_str = header_value("Content-Length"); @@ -16,7 +16,7 @@ std::optional http_response_info::content_length() const noexcept { int clen = 0; auto conv_res = std::from_chars(cl_str->data(), cl_str->data() + cl_str->size(), clen); if (conv_res.ec != std::errc{}) { - dds_log(warn, + bpt_log(warn, "The HTTP server returned a non-integral 'Content-Length' header: '{}'. We'll " "pretend that there is no 'Content-Length' on this message.", *cl_str); diff --git a/src/dds/util/http/response.hpp b/src/bpt/util/http/response.hpp similarity index 94% rename from src/dds/util/http/response.hpp rename to src/bpt/util/http/response.hpp index 24fa7cf6..b37d1a89 100644 --- a/src/dds/util/http/response.hpp +++ b/src/bpt/util/http/response.hpp @@ -5,7 +5,7 @@ #include -namespace dds { +namespace bpt { struct http_response_info { int status; @@ -15,8 +15,6 @@ struct http_response_info { std::size_t head_byte_size = 0; - void throw_for_status() const; - bool is_client_error() const noexcept { return status >= 400 && status < 500; } bool is_server_error() const noexcept { return status >= 500 && status < 600; } bool is_error() const noexcept { return is_client_error() || is_server_error(); } @@ -32,4 +30,4 @@ struct http_response_info { auto last_modified() const noexcept { return header_value("Last-Modified"); } }; -} // namespace dds +} // namespace bpt diff --git a/src/bpt/util/json5/convert.cpp b/src/bpt/util/json5/convert.cpp new file mode 100644 index 00000000..6295ab80 --- /dev/null +++ b/src/bpt/util/json5/convert.cpp @@ -0,0 +1,58 @@ +#include "./convert.hpp" + +#include +#include + +using namespace bpt; + +nlohmann::json bpt::json5_as_nlohmann_json(const json5::data& dat) noexcept { + if (dat.is_null()) { + return nullptr; + } else if (dat.is_string()) { + return dat.as_string(); + } else if (dat.is_number()) { + return dat.as_number(); + } else if (dat.is_boolean()) { + return dat.as_boolean(); + } else if (dat.is_array()) { + auto ret = nlohmann::json::array(); + for (auto&& elem : dat.as_array()) { + ret.push_back(json5_as_nlohmann_json(elem)); + } + return ret; + } else if (dat.is_object()) { + auto ret = nlohmann::json::object(); + for (auto&& [key, value] : dat.as_object()) { + ret.emplace(key, json5_as_nlohmann_json(value)); + } + return ret; + } else { + neo::unreachable(); + } +} + +json5::data bpt::nlohmann_json_as_json5(const nlohmann::json& data) noexcept { + if (data.is_null()) { + return nullptr; + } else if (data.is_string()) { + return std::string(data); + } else if (data.is_number()) { + return double(data); + } else if (data.is_boolean()) { + return bool(data); + } else if (data.is_array()) { + auto ret = json5::data::array_type{}; + for (const nlohmann::json& elem : data) { + ret.push_back(nlohmann_json_as_json5(elem)); + } + return json5::data(std::move(ret)); + } else if (data.is_object()) { + auto ret = json5::data::mapping_type{}; + for (const auto& [key, value] : data.items()) { + ret.emplace(key, nlohmann_json_as_json5(value)); + } + return json5::data(std::move(ret)); + } else { + neo::unreachable(); + } +} diff --git a/src/bpt/util/json5/convert.hpp b/src/bpt/util/json5/convert.hpp new file mode 100644 index 00000000..42b5c0e6 --- /dev/null +++ b/src/bpt/util/json5/convert.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include +#include + +namespace bpt { + +nlohmann::json json5_as_nlohmann_json(const json5::data& data) noexcept; +json5::data nlohmann_json_as_json5(const nlohmann::json&) noexcept; + +} // namespace bpt diff --git a/src/bpt/util/json5/convert.test.cpp b/src/bpt/util/json5/convert.test.cpp new file mode 100644 index 00000000..610db8d0 --- /dev/null +++ b/src/bpt/util/json5/convert.test.cpp @@ -0,0 +1,26 @@ +#include "./convert.hpp" + +#include + +#include + +TEST_CASE("Convert to JSON5") { + auto data = nlohmann::json::object({ + {"name", "Joe"}, + {"address", "Main Street"}, + }); + + auto j5 = bpt::nlohmann_json_as_json5(data); + REQUIRE(j5.is_object()); + auto& map = j5.as_object(); + + auto it = map.find("name"); + CHECKED_IF(it != map.cend()) { + CHECKED_IF(it->second.is_string()) { REQUIRE(it->second.as_string() == "Joe"); } + } + + it = map.find("address"); + CHECKED_IF(it != map.cend()) { + CHECKED_IF(it->second.is_string()) { CHECK(it->second.as_string() == "Main Street"); } + } +} diff --git a/src/bpt/util/json5/error.hpp b/src/bpt/util/json5/error.hpp new file mode 100644 index 00000000..82cbcbf4 --- /dev/null +++ b/src/bpt/util/json5/error.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include + +namespace bpt { + +struct e_json_string { + std::string value; +}; + +struct e_json5_string { + std::string value; +}; + +struct e_json_parse_error { + std::string value; +}; + +} // namespace bpt \ No newline at end of file diff --git a/src/bpt/util/json5/parse.cpp b/src/bpt/util/json5/parse.cpp new file mode 100644 index 00000000..a7c264a1 --- /dev/null +++ b/src/bpt/util/json5/parse.cpp @@ -0,0 +1,43 @@ +#include "./parse.hpp" + +#include "./error.hpp" + +#include + +#include + +#include +#include +#include + +using namespace bpt; + +nlohmann::json bpt::parse_json_file(std::filesystem::path const& fpath) { + BPT_E_SCOPE(fpath); + auto content = bpt::read_file(fpath); + return parse_json_str(content); +} + +nlohmann::json bpt::parse_json_str(std::string_view content) { + BPT_E_SCOPE(e_json_string{std::string(content)}); + try { + return nlohmann::json::parse(content); + } catch (const nlohmann::json::exception& err) { + BOOST_LEAF_THROW_EXCEPTION(err, e_json_parse_error{err.what()}); + } +} + +json5::data bpt::parse_json5_file(std::filesystem::path const& fpath) { + BPT_E_SCOPE(fpath); + auto content = bpt::read_file(fpath); + return parse_json5_str(content); +} + +json5::data bpt::parse_json5_str(std::string_view content) { + BPT_E_SCOPE(e_json5_string{std::string(content)}); + try { + return json5::parse_data(content); + } catch (const json5::parse_error& err) { + BOOST_LEAF_THROW_EXCEPTION(err, e_json_parse_error{err.what()}); + } +} diff --git a/src/bpt/util/json5/parse.hpp b/src/bpt/util/json5/parse.hpp new file mode 100644 index 00000000..de355812 --- /dev/null +++ b/src/bpt/util/json5/parse.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +#include +#include + +namespace bpt { + +nlohmann::json parse_json_str(std::string_view); +nlohmann::json parse_json_file(std::filesystem::path const&); +json5::data parse_json5_str(std::string_view); +json5::data parse_json5_file(std::filesystem::path const&); + +} // namespace bpt diff --git a/src/bpt/util/json5/parse.test.cpp b/src/bpt/util/json5/parse.test.cpp new file mode 100644 index 00000000..e2a6498a --- /dev/null +++ b/src/bpt/util/json5/parse.test.cpp @@ -0,0 +1,32 @@ +#include "./error.hpp" +#include "./parse.hpp" + +#include +#include + +#include +#include + +TEST_CASE("Test parse JSON content") { + bpt_leaf_try { + bpt::parse_json_str(R"({foo})"); + FAIL_CHECK("Expected an error"); + } + bpt_leaf_catch(bpt::e_json_parse_error) {} + bpt_leaf_catch_all { FAIL_CHECK("Incorrect error: " << diagnostic_info); }; + + auto result = REQUIRES_LEAF_NOFAIL(bpt::parse_json_str(R"({"foo": "bar"})")); + CHECK(result.is_object()); +} + +TEST_CASE("Test parse JSON5 content") { + bpt_leaf_try { + bpt::parse_json5_str("{foo}"); + FAIL_CHECK("Expected an error"); + } + bpt_leaf_catch(bpt::e_json_parse_error) {} + bpt_leaf_catch_all { FAIL_CHECK("Incorrect error: " << diagnostic_info); }; + + auto result = REQUIRES_LEAF_NOFAIL(bpt::parse_json5_str("{foo: 'bar'}")); + CHECK(result.is_object()); +} diff --git a/src/bpt/util/json_walk.cpp b/src/bpt/util/json_walk.cpp new file mode 100644 index 00000000..10a67942 --- /dev/null +++ b/src/bpt/util/json_walk.cpp @@ -0,0 +1,14 @@ +#include "./json_walk.hpp" + +#include +#include + +#include + +bpt::name bpt::walk_utils::name_from_string::operator()(std::string s) { + return bpt::name::from_string(s).value(); +} + +semver::version bpt::walk_utils::version_from_string::operator()(std::string s) { + return semver::version::parse(s); +} diff --git a/src/bpt/util/json_walk.hpp b/src/bpt/util/json_walk.hpp new file mode 100644 index 00000000..dd9c60c9 --- /dev/null +++ b/src/bpt/util/json_walk.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace semver { +struct version; +} + +namespace bpt { +struct name; +} + +namespace bpt::walk_utils { + +using namespace semester::walk_ops; +using semester::walk_error; + +using json5_mapping = json5::data::mapping_type; +using json5_array = json5::data::array_type; +using require_mapping = semester::require_type; +using require_array = semester::require_type; +using require_str = semester::require_type; + +struct name_from_string { + bpt::name operator()(std::string s); +}; + +struct version_from_string { + semver::version operator()(std::string s); +}; + +struct key_dym_tracker { + std::set> known_keys; + std::set> seen_keys = {}; + + auto tracker() { + return [this](auto&& key, auto&&) { + seen_keys.emplace(std::string(key)); + return walk.pass; + }; + } + + template + auto rejecter() { + return [this](auto&& key, auto&&) -> semester::walk_result { + auto unseen + = known_keys | std::views::filter([&](auto k) { return !seen_keys.contains(k); }); + BOOST_LEAF_THROW_EXCEPTION(E{std::string(key), did_you_mean(key, unseen)}); + neo::unreachable(); + }; + } +}; + +struct set_true { + bool& b; + + auto operator()(auto&&) const { + b = true; + return walk.pass; + } +}; + +} // namespace bpt::walk_utils diff --git a/src/dds/util/log.cpp b/src/bpt/util/log.cpp similarity index 88% rename from src/dds/util/log.cpp rename to src/bpt/util/log.cpp index a83746d1..2a1bf64d 100644 --- a/src/dds/util/log.cpp +++ b/src/bpt/util/log.cpp @@ -19,14 +19,14 @@ static void set_utf8_output() { } #endif -void dds::log::init_logger() noexcept { +void bpt::log::init_logger() noexcept { // spdlog::set_pattern("[%H:%M:%S] [%^%-5l%$] %v"); spdlog::set_pattern("[%^%-5l%$] %v"); } -void dds::log::ev_log::print() const noexcept { log_print(level, message); } +void bpt::log::ev_log::print() const noexcept { log_print(level, message); } -void dds::log::log_print(dds::log::level l, std::string_view msg) noexcept { +void bpt::log::log_print(bpt::log::level l, std::string_view msg) noexcept { static auto logger_inst = [] { auto logger = spdlog::default_logger_raw(); logger->set_level(spdlog::level::trace); @@ -57,7 +57,7 @@ void dds::log::log_print(dds::log::level l, std::string_view msg) noexcept { logger_inst->log(lvl, msg); } -void dds::log::log_emit(dds::log::ev_log ev) noexcept { +void bpt::log::log_emit(bpt::log::ev_log ev) noexcept { if (!neo::get_event_subscriber()) { thread_local bool did_warn = false; if (!did_warn) { diff --git a/src/dds/util/log.hpp b/src/bpt/util/log.hpp similarity index 84% rename from src/dds/util/log.hpp rename to src/bpt/util/log.hpp index 1d9b8c97..3d10208a 100644 --- a/src/dds/util/log.hpp +++ b/src/bpt/util/log.hpp @@ -4,7 +4,7 @@ #include -namespace dds::log { +namespace bpt::log { enum class level : int { trace, @@ -50,11 +50,11 @@ void trace(std::string_view s, const Args&... args) { log(level::trace, s, args...); } -#define dds_log(Level, str, ...) \ +#define bpt_log(Level, str, ...) \ do { \ - if (int(dds::log::level::Level) >= int(dds::log::current_log_level)) { \ - ::dds::log::log(::dds::log::level::Level, str __VA_OPT__(, ) __VA_ARGS__); \ + if (int(bpt::log::level::Level) >= int(bpt::log::current_log_level)) { \ + ::bpt::log::log(::bpt::log::level::Level, str __VA_OPT__(, ) __VA_ARGS__); \ } \ } while (0) -} // namespace dds::log \ No newline at end of file +} // namespace bpt::log \ No newline at end of file diff --git a/src/bpt/util/name.cpp b/src/bpt/util/name.cpp new file mode 100644 index 00000000..7d203dc6 --- /dev/null +++ b/src/bpt/util/name.cpp @@ -0,0 +1,80 @@ +#include "./name.hpp" + +#include +#include + +#include + +#include + +using namespace bpt; + +using err_reason = invalid_name_reason; + +static err_reason calc_invalid_name_reason(std::string_view str) noexcept { + constexpr ctll::fixed_string capital_re = "[A-Z]"; + constexpr ctll::fixed_string double_punct = "[._\\-]{2}"; + constexpr ctll::fixed_string end_punct = "[._\\-]$"; + constexpr ctll::fixed_string ws = "\\s"; + constexpr ctll::fixed_string invalid_chars = "[^a-z0-9._\\-]"; + if (str.empty()) { + return err_reason::empty; + } else if (ctre::search(str)) { + return err_reason::capital; + } else if (ctre::search(str)) { + return err_reason::double_punct; + } else if (str[0] < 'a' || str[0] > 'z') { + return err_reason::initial_not_alpha; + } else if (ctre::search(str)) { + return err_reason::end_punct; + } else if (ctre::search(str)) { + return err_reason::whitespace; + } else if (ctre::search(str)) { + return err_reason::invalid_char; + } else { + neo_assert(invariant, + false, + "Expected to be able to determine an error-reason for the given invalid name", + str); + } +} + +std::string_view bpt::invalid_name_reason_str(err_reason e) noexcept { + switch (e) { + case err_reason::capital: + return "Uppercase letters are not valid in names"; + case err_reason::double_punct: + return "Adjacent punctuation characters are not valid in names"; + case err_reason::end_punct: + return "Names must not end with a punctuation character"; + case err_reason::whitespace: + return "Names must not contain whitespace"; + case err_reason::invalid_char: + return "Name contains an invalid character"; + case err_reason::initial_not_alpha: + return "Name must begin with a lowercase alphabetic character"; + case err_reason::empty: + return "Name cannot be empty"; + } + neo::unreachable(); +} + +result name::from_string(std::string_view str) noexcept { + constexpr ctll::fixed_string name_re = "^([a-z][a-z0-9]*)([._\\-][a-z0-9]+)*$"; + + auto mat = ctre::match(str); + + if (!mat) { + return new_error(BPT_E_ARG(e_name_str{std::string(str)}), + BPT_E_ARG(calc_invalid_name_reason(str))); + } + + return name{std::string(str)}; +} + +std::ostream& bpt::operator<<(std::ostream& out, invalid_name_reason r) { + out << invalid_name_reason_str(r); + return out; +} + +void name::write_to(std::ostream& out) const noexcept { out << str; } \ No newline at end of file diff --git a/src/bpt/util/name.hpp b/src/bpt/util/name.hpp new file mode 100644 index 00000000..684bdd9c --- /dev/null +++ b/src/bpt/util/name.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include + +#include +#include + +namespace bpt { + +enum class invalid_name_reason { + empty, + capital, + initial_not_alpha, + double_punct, + end_punct, + invalid_char, + whitespace, +}; + +std::ostream& operator<<(std::ostream& out, invalid_name_reason); + +struct e_name_str { + std::string value; +}; + +std::string_view invalid_name_reason_str(invalid_name_reason) noexcept; + +struct name { + std::string str; + + // Parse a package name, ensuring it is a valid package name string + static result from_string(std::string_view str) noexcept; + + auto operator<=>(const name&) const noexcept = default; + + void write_to(std::ostream&) const noexcept; + + friend std::ostream& operator<<(std::ostream& o, const name& self) noexcept { + self.write_to(o); + return o; + } + + friend void do_repr(auto out, const name* self) { + out.type("bpt::name"); + if (self) { + out.value("{}", out.repr_value(self->str)); + } + } +}; + +} // namespace bpt diff --git a/src/bpt/util/name.test.cpp b/src/bpt/util/name.test.cpp new file mode 100644 index 00000000..68e5d50a --- /dev/null +++ b/src/bpt/util/name.test.cpp @@ -0,0 +1,72 @@ +#include "./name.hpp" + +#include + +#include + +#include + +TEST_CASE("Try some invalid names") { + using reason = bpt::invalid_name_reason; + struct case_ { + std::string_view invalid_name; + bpt::invalid_name_reason error; + }; + auto given = GENERATE(Catch::Generators::values({ + {"", reason::empty}, + + {"H", reason::capital}, + {"heLlo", reason::capital}, + {"eGGG", reason::capital}, + {"e0131F-gg", reason::capital}, + + {"-foo", reason::initial_not_alpha}, + {"123", reason::initial_not_alpha}, + {"123-bar", reason::initial_not_alpha}, + {" fooo", reason::initial_not_alpha}, + + {"foo..bar", reason::double_punct}, + {"foo-.bar", reason::double_punct}, + {"foo-_bar", reason::double_punct}, + {"foo__bar", reason::double_punct}, + {"foo_.bar", reason::double_punct}, + + {"foo.", reason::end_punct}, + {"foo.bar_", reason::end_punct}, + {"foo.bar-", reason::end_punct}, + + {"foo ", reason::whitespace}, + {"foo bar", reason::whitespace}, + {"foo\nbar", reason::whitespace}, + + {"foo&bar", reason::invalid_char}, + {"foo+baz", reason::invalid_char}, + })); + + boost::leaf::context err_ctx; + err_ctx.activate(); + CAPTURE(given.invalid_name); + auto res = bpt::name::from_string(given.invalid_name); + err_ctx.deactivate(); + CHECKED_IF(!res) { + err_ctx.handle_error( + res.error(), + [&](reason r) { CHECK(r == given.error); }, + [] { FAIL_CHECK("No error reason was given"); }); + } +} + +TEST_CASE("Try some valid names") { + auto given = GENERATE(Catch::Generators::values({ + "hi", + "dog", + "foo.bar", + "foo-bar_bark", + "foo-bar-baz", + "foo-bar.quz", + "q", + })); + + auto res = bpt::name::from_string(given); + CHECK(res->str == given); +} diff --git a/src/dds/util/output.hpp b/src/bpt/util/output.hpp similarity index 70% rename from src/dds/util/output.hpp rename to src/bpt/util/output.hpp index ffc0f7e1..caaead28 100644 --- a/src/dds/util/output.hpp +++ b/src/bpt/util/output.hpp @@ -1,9 +1,9 @@ #pragma once -namespace dds { +namespace bpt { void enable_ansi_console() noexcept; bool stdout_is_a_tty() noexcept; -} // namespace dds +} // namespace bpt diff --git a/src/bpt/util/output.nix.cpp b/src/bpt/util/output.nix.cpp new file mode 100644 index 00000000..9e9af98d --- /dev/null +++ b/src/bpt/util/output.nix.cpp @@ -0,0 +1,15 @@ +#if !_WIN32 + +#include + +#include + +using namespace bpt; + +void bpt::enable_ansi_console() noexcept { + // unix consoles generally already support ANSI control chars by default +} + +bool bpt::stdout_is_a_tty() noexcept { return ::isatty(STDOUT_FILENO) != 0; } + +#endif diff --git a/src/dds/util/output.win.cpp b/src/bpt/util/output.win.cpp similarity index 87% rename from src/dds/util/output.win.cpp rename to src/bpt/util/output.win.cpp index 6c42716c..1f0de2d6 100644 --- a/src/dds/util/output.win.cpp +++ b/src/bpt/util/output.win.cpp @@ -1,10 +1,10 @@ -#include +#include #if _WIN32 #include -void dds::enable_ansi_console() noexcept { +void bpt::enable_ansi_console() noexcept { auto stdio_console = ::GetStdHandle(STD_OUTPUT_HANDLE); if (stdio_console == INVALID_HANDLE_VALUE) { // Oh well... @@ -20,7 +20,7 @@ void dds::enable_ansi_console() noexcept { ::SetConsoleMode(stdio_console, mode); } -bool dds::stdout_is_a_tty() noexcept { +bool bpt::stdout_is_a_tty() noexcept { auto stdio_console = ::GetStdHandle(STD_OUTPUT_HANDLE); if (stdio_console == INVALID_HANDLE_VALUE) { return false; diff --git a/src/bpt/util/parallel.cpp b/src/bpt/util/parallel.cpp new file mode 100644 index 00000000..5a4cee02 --- /dev/null +++ b/src/bpt/util/parallel.cpp @@ -0,0 +1,17 @@ +#include "./parallel.hpp" + +#include + +#include + +using namespace bpt; + +void bpt::log_exception(std::exception_ptr eptr) noexcept { + try { + std::rethrow_exception(eptr); + } catch (const bpt::user_cancelled&) { + // Don't log this one. The user knows what they did + } catch (const std::exception& e) { + bpt_log(error, "{}", e.what()); + } +} diff --git a/src/dds/util/parallel.hpp b/src/bpt/util/parallel.hpp similarity index 92% rename from src/dds/util/parallel.hpp rename to src/bpt/util/parallel.hpp index 2f709950..cb43f09e 100644 --- a/src/dds/util/parallel.hpp +++ b/src/bpt/util/parallel.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include #include @@ -11,7 +11,7 @@ #include #include -namespace dds { +namespace bpt { void log_exception(std::exception_ptr) noexcept; @@ -27,7 +27,7 @@ bool parallel_run(Range&& rng, int n_jobs, Func&& fn) { std::vector exceptions; auto run_one = [&]() mutable { - auto log_subscr = neo::subscribe(&log::ev_log::print); + neo::listener log_listen = &log::ev_log::print; while (true) { std::unique_lock lk{mut}; @@ -66,4 +66,4 @@ bool parallel_run(Range&& rng, int n_jobs, Func&& fn) { return exceptions.empty(); } -} // namespace dds \ No newline at end of file +} // namespace bpt \ No newline at end of file diff --git a/src/bpt/util/parse_enum.hpp b/src/bpt/util/parse_enum.hpp new file mode 100644 index 00000000..75b2db85 --- /dev/null +++ b/src/bpt/util/parse_enum.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include + +#include +#include + +#include + +namespace bpt { + +template +struct e_invalid_enum { + std::string value; +}; + +struct e_invalid_enum_str { + std::string value; +}; + +struct e_enum_options { + std::string value; +}; + +template +constexpr auto parse_enum_str = [](const std::string& sv) { + auto e = magic_enum::enum_cast(sv); + if (e.has_value()) { + return *e; + } + + BOOST_LEAF_THROW_EXCEPTION( // + e_invalid_enum{std::string(magic_enum::enum_type_name())}, + e_invalid_enum_str{std::string(sv)}, + [&] { + auto names = magic_enum::enum_names(); + std::string acc + = bpt::joinstr(", ", names | std::views::transform([](auto n) { + return std::string{"\""} + std::string(n) + "\""; + })); + return e_enum_options{acc}; + }); +}; + +} // namespace bpt \ No newline at end of file diff --git a/src/bpt/util/paths.hpp b/src/bpt/util/paths.hpp new file mode 100644 index 00000000..4d612b01 --- /dev/null +++ b/src/bpt/util/paths.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace bpt { + +fs::path user_home_dir(); +fs::path user_data_dir(); +fs::path user_cache_dir(); +fs::path user_config_dir(); + +inline fs::path bpt_data_dir() { return user_data_dir() / "bpt"; } +inline fs::path bpt_cache_dir() { return user_cache_dir() / "bpt"; } +inline fs::path bpt_config_dir() { return user_config_dir() / "bpt"; } + +} // namespace bpt \ No newline at end of file diff --git a/src/dds/util/paths.linux_fbsd.cpp b/src/bpt/util/paths.linux_fbsd.cpp similarity index 51% rename from src/dds/util/paths.linux_fbsd.cpp rename to src/bpt/util/paths.linux_fbsd.cpp index a0683e91..b2eff731 100644 --- a/src/dds/util/paths.linux_fbsd.cpp +++ b/src/bpt/util/paths.linux_fbsd.cpp @@ -2,43 +2,43 @@ #include "./paths.hpp" -#include -#include +#include +#include #include -using namespace dds; +using namespace bpt; -fs::path dds::user_home_dir() { +fs::path bpt::user_home_dir() { static auto ret = []() -> fs::path { - return fs::absolute(dds::getenv("HOME", [] { - dds_log(error, "No HOME environment variable set!"); + return fs::absolute(bpt::getenv("HOME", [] { + bpt_log(error, "No HOME environment variable set!"); return "/"; })); }(); return ret; } -fs::path dds::user_data_dir() { +fs::path bpt::user_data_dir() { static auto ret = []() -> fs::path { return fs::absolute( - dds::getenv("XDG_DATA_HOME", [] { return user_home_dir() / ".local/share"; })); + bpt::getenv("XDG_DATA_HOME", [] { return user_home_dir() / ".local/share"; })); }(); return ret; } -fs::path dds::user_cache_dir() { +fs::path bpt::user_cache_dir() { static auto ret = []() -> fs::path { return fs::absolute( - dds::getenv("XDG_CACHE_HOME", [] { return user_home_dir() / ".cache"; })); + bpt::getenv("XDG_CACHE_HOME", [] { return user_home_dir() / ".cache"; })); }(); return ret; } -fs::path dds::user_config_dir() { +fs::path bpt::user_config_dir() { static auto ret = []() -> fs::path { return fs::absolute( - dds::getenv("XDG_CONFIG_HOME", [] { return user_home_dir() / ".config"; })); + bpt::getenv("XDG_CONFIG_HOME", [] { return user_home_dir() / ".config"; })); }(); return ret; } diff --git a/src/bpt/util/paths.macos.cpp b/src/bpt/util/paths.macos.cpp new file mode 100644 index 00000000..5538cc66 --- /dev/null +++ b/src/bpt/util/paths.macos.cpp @@ -0,0 +1,47 @@ +#ifdef __APPLE__ + +#include "./paths.hpp" + +#include +#include + +#include + +using namespace bpt; + +fs::path bpt::user_home_dir() { + static auto ret = []() -> fs::path { + return fs::absolute(bpt::getenv("HOME", [] { + bpt_log(error, "No HOME environment variable set!"); + return "/"; + })); + }(); + return ret; +} + +fs::path bpt::user_data_dir() { + static auto ret = []() -> fs::path { + return fs::absolute(bpt::getenv("XDG_DATA_HOME", [] { + return user_home_dir() / "Library/Application Support"; + })); + }(); + return ret; +} + +fs::path bpt::user_cache_dir() { + static auto ret = []() -> fs::path { + return fs::absolute( + bpt::getenv("XDG_CACHE_HOME", [] { return user_home_dir() / "Library/Caches"; })); + }(); + return ret; +} + +fs::path bpt::user_config_dir() { + static auto ret = []() -> fs::path { + return fs::absolute( + bpt::getenv("XDG_CONFIG_HOME", [] { return user_home_dir() / "Library/Preferences"; })); + }(); + return ret; +} + +#endif diff --git a/src/dds/util/paths.win.cpp b/src/bpt/util/paths.win.cpp similarity index 83% rename from src/dds/util/paths.win.cpp rename to src/bpt/util/paths.win.cpp index 5e0270de..a174bf5d 100644 --- a/src/dds/util/paths.win.cpp +++ b/src/bpt/util/paths.win.cpp @@ -2,14 +2,14 @@ #include "./paths.hpp" -#include +#include #include #include #include -using namespace dds; +using namespace bpt; namespace { @@ -37,7 +37,7 @@ getenv_wstr(std::wstring varname, std::wstring default_val, std::size_t size_hin } // namespace -fs::path dds::user_home_dir() { +fs::path bpt::user_home_dir() { static auto ret = []() -> fs::path { std::wstring userprofile_env = getenv_wstr(L"UserProfile", L"/"); return fs::absolute(fs::path(userprofile_env)); @@ -59,8 +59,8 @@ fs::path appdata_dir() { } // namespace -fs::path dds::user_data_dir() { return appdatalocal_dir(); } -fs::path dds::user_cache_dir() { return appdatalocal_dir(); } -fs::path dds::user_config_dir() { return appdata_dir(); } +fs::path bpt::user_data_dir() { return appdatalocal_dir(); } +fs::path bpt::user_cache_dir() { return appdatalocal_dir(); } +fs::path bpt::user_config_dir() { return appdata_dir(); } #endif diff --git a/src/dds/proc.common.cpp b/src/bpt/util/proc.common.cpp similarity index 76% rename from src/dds/proc.common.cpp rename to src/bpt/util/proc.common.cpp index d12e6043..ec1531aa 100644 --- a/src/dds/proc.common.cpp +++ b/src/bpt/util/proc.common.cpp @@ -1,13 +1,13 @@ #include "./proc.hpp" -#include +#include #include #include -using namespace dds; +using namespace bpt; -bool dds::needs_quoting(std::string_view s) { +bool bpt::needs_quoting(std::string_view s) { std::string_view okay_chars = "@%-+=:,./|_"; const bool all_okay = std::all_of(s.begin(), s.end(), [&](char c) { return std::isalnum(c) || (okay_chars.find(c) != okay_chars.npos); @@ -15,7 +15,7 @@ bool dds::needs_quoting(std::string_view s) { return !all_okay; } -std::string dds::quote_argument(std::string_view s) { +std::string bpt::quote_argument(std::string_view s) { if (!needs_quoting(s)) { return std::string(s); } diff --git a/src/dds/proc.hpp b/src/bpt/util/proc.hpp similarity index 96% rename from src/dds/proc.hpp rename to src/bpt/util/proc.hpp index c41ccd20..ca4981aa 100644 --- a/src/dds/proc.hpp +++ b/src/bpt/util/proc.hpp @@ -7,7 +7,7 @@ #include #include -namespace dds { +namespace bpt { bool needs_quoting(std::string_view); @@ -51,4 +51,4 @@ inline proc_result run_proc(std::vector args) { return run_proc(proc_options{.command = std::move(args)}); } -} // namespace dds +} // namespace bpt diff --git a/src/dds/proc.nix.cpp b/src/bpt/util/proc.nix.cpp similarity index 87% rename from src/dds/proc.nix.cpp rename to src/bpt/util/proc.nix.cpp index 5f9d76a6..407f228d 100644 --- a/src/dds/proc.nix.cpp +++ b/src/bpt/util/proc.nix.cpp @@ -1,9 +1,12 @@ -#ifndef _WIN32 #include "./proc.hpp" -#include -#include -#include +#ifndef _WIN32 + +#include +#include +#include + +#include #include #include @@ -16,7 +19,7 @@ #include #include -using namespace dds; +using namespace bpt; namespace { @@ -38,7 +41,7 @@ ::pid_t spawn_child(const proc_options& opts, int stdout_pipe, int close_me) noe std::string workdir = opts.cwd.value_or(fs::current_path()).string(); auto not_found_err - = fmt::format("[dds child executor] The requested executable [{}] could not be found.", + = fmt::format("[bpt child executor] The requested executable [{}] could not be found.", strings[0]); auto child_pid = ::fork(); @@ -61,7 +64,7 @@ ::pid_t spawn_child(const proc_options& opts, int stdout_pipe, int close_me) noe std::_Exit(-1); } - std::fputs("[dds child executor] execvp returned! This is a fatal error: ", stderr); + std::fputs("[bpt child executor] execvp returned! This is a fatal error: ", stderr); std::fputs(std::strerror(errno), stderr); std::fputs("\n", stderr); std::_Exit(-1); @@ -69,8 +72,8 @@ ::pid_t spawn_child(const proc_options& opts, int stdout_pipe, int close_me) noe } // namespace -proc_result dds::run_proc(const proc_options& opts) { - dds_log(debug, "Spawning subprocess: {}", quote_command(opts.command)); +proc_result bpt::run_proc(const proc_options& opts) { + bpt_log(debug, "Spawning subprocess: {}", quote_command(opts.command)); int stdio_pipe[2] = {}; auto rc = ::pipe(stdio_pipe); check_rc(rc == 0, "Create stdio pipe for subprocess"); @@ -78,6 +81,8 @@ proc_result dds::run_proc(const proc_options& opts) { int read_pipe = stdio_pipe[0]; int write_pipe = stdio_pipe[1]; + neo_defer { ::close(read_pipe); }; + auto child = spawn_child(opts, write_pipe, read_pipe); ::close(write_pipe); @@ -108,7 +113,7 @@ proc_result dds::run_proc(const proc_options& opts) { ::kill(child, SIGINT); timeout = -1ms; res.timed_out = true; - dds_log(debug, "Subprocess [{}] timed out", quote_command(opts.command)); + bpt_log(debug, "Subprocess [{}] timed out", quote_command(opts.command)); continue; } std::string buffer; @@ -135,4 +140,4 @@ proc_result dds::run_proc(const proc_options& opts) { return res; } -#endif // _WIN32 \ No newline at end of file +#endif // _WIN32 diff --git a/src/dds/proc.win.cpp b/src/bpt/util/proc.win.cpp similarity index 95% rename from src/dds/proc.win.cpp rename to src/bpt/util/proc.win.cpp index 0b8ee1c5..09cedb2e 100644 --- a/src/dds/proc.win.cpp +++ b/src/bpt/util/proc.win.cpp @@ -1,8 +1,9 @@ -#ifdef _WIN32 #include "./proc.hpp" -#include -#include +#ifdef _WIN32 + +#include +#include #include #include @@ -15,7 +16,7 @@ #include #include -using namespace dds; +using namespace bpt; using namespace std::chrono_literals; namespace { @@ -38,10 +39,10 @@ std::wstring widen(std::string_view s) { } // namespace -proc_result dds::run_proc(const proc_options& opts) { +proc_result bpt::run_proc(const proc_options& opts) { auto cmd_str = quote_command(opts.command); auto cmd_wide = widen(cmd_str); - dds_log(debug, "Spawning subprocess: {}", cmd_str); + bpt_log(debug, "Spawning subprocess: {}", cmd_str); ::SECURITY_ATTRIBUTES security = {}; security.bInheritHandle = TRUE; diff --git a/src/bpt/util/result.hpp b/src/bpt/util/result.hpp new file mode 100644 index 00000000..5089d910 --- /dev/null +++ b/src/bpt/util/result.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace bpt { + +using boost::leaf::current_error; +using boost::leaf::error_id; +using boost::leaf::new_error; +using boost::leaf::result; + +template U> +constexpr T value_or(const result& res, U&& arg) { + return res ? res.value() : static_cast(arg); +} + +void write_error_marker(std::string_view error) noexcept; + +} // namespace bpt diff --git a/src/dds/util/shlex.cpp b/src/bpt/util/shlex.cpp similarity index 95% rename from src/dds/util/shlex.cpp rename to src/bpt/util/shlex.cpp index 379e5013..8120d0aa 100644 --- a/src/dds/util/shlex.cpp +++ b/src/bpt/util/shlex.cpp @@ -7,9 +7,9 @@ using std::string; using std::vector; -using namespace dds; +using namespace bpt; -vector dds::split_shell_string(std::string_view shell) { +vector bpt::split_shell_string(std::string_view shell) { char cur_quote = 0; bool is_escaped = false; diff --git a/src/dds/util/shlex.hpp b/src/bpt/util/shlex.hpp similarity index 80% rename from src/dds/util/shlex.hpp rename to src/bpt/util/shlex.hpp index 3cf59603..96aa4b38 100644 --- a/src/dds/util/shlex.hpp +++ b/src/bpt/util/shlex.hpp @@ -4,8 +4,8 @@ #include #include -namespace dds { +namespace bpt { std::vector split_shell_string(std::string_view s); -} // namespace dds \ No newline at end of file +} // namespace bpt \ No newline at end of file diff --git a/src/dds/util/shlex.test.cpp b/src/bpt/util/shlex.test.cpp similarity index 91% rename from src/dds/util/shlex.test.cpp rename to src/bpt/util/shlex.test.cpp index 650c1135..8a1ec990 100644 --- a/src/dds/util/shlex.test.cpp +++ b/src/bpt/util/shlex.test.cpp @@ -1,11 +1,11 @@ -#include +#include #include #define CHECK_SHLEX(str, ...) \ do { \ INFO("Shell-lexing string: '" << str << "'"); \ - CHECK(dds::split_shell_string(str) == std::vector(__VA_ARGS__)); \ + CHECK(bpt::split_shell_string(str) == std::vector(__VA_ARGS__)); \ } while (0) TEST_CASE("Shell lexer") { diff --git a/src/dds/util/signal.cpp b/src/bpt/util/signal.cpp similarity index 73% rename from src/dds/util/signal.cpp rename to src/bpt/util/signal.cpp index ea164ab6..0136addd 100644 --- a/src/dds/util/signal.cpp +++ b/src/bpt/util/signal.cpp @@ -10,11 +10,11 @@ void handle_signal(int sig) { got_signal = sig; } } // namespace -using namespace dds; +using namespace bpt; -void dds::notify_cancel() noexcept { got_signal = SIGINT; } +void bpt::notify_cancel() noexcept { got_signal = SIGINT; } -void dds::install_signal_handlers() noexcept { +void bpt::install_signal_handlers() noexcept { std::signal(SIGINT, handle_signal); std::signal(SIGTERM, handle_signal); @@ -30,8 +30,8 @@ void dds::install_signal_handlers() noexcept { #endif } -bool dds::is_cancelled() noexcept { return got_signal != 0; } -void dds::cancellation_point() { +bool bpt::is_cancelled() noexcept { return got_signal != 0; } +void bpt::cancellation_point() { if (is_cancelled()) { throw user_cancelled(); } diff --git a/src/dds/util/signal.hpp b/src/bpt/util/signal.hpp similarity index 87% rename from src/dds/util/signal.hpp rename to src/bpt/util/signal.hpp index 9c2fa176..542befaa 100644 --- a/src/dds/util/signal.hpp +++ b/src/bpt/util/signal.hpp @@ -2,7 +2,7 @@ #include -namespace dds { +namespace bpt { class user_cancelled : public std::exception {}; @@ -13,4 +13,4 @@ void reset_cancelled() noexcept; bool is_cancelled() noexcept; void cancellation_point(); -} // namespace dds \ No newline at end of file +} // namespace bpt \ No newline at end of file diff --git a/src/bpt/util/siphash.hpp b/src/bpt/util/siphash.hpp new file mode 100644 index 00000000..5f11f160 --- /dev/null +++ b/src/bpt/util/siphash.hpp @@ -0,0 +1,123 @@ +#pragma once + +#include +#include + +#include +#include + +namespace bpt { + +/** + * @brief Implementation of SipHash that computes a 64-bit digest + */ +class siphash64 { + using u64 = std::uint64_t; + + u64 _digest = 0; + + /// Read a little-endien u64 integer from the given buffer + constexpr static u64 _read_u64_le(neo::const_buffer buf) noexcept { + std::byte bytes[8] = {}; + neo::buffer_copy(neo::mutable_buffer(bytes), buf); + u64 m = 0; + for (auto i = 8; i;) { + m <<= 8; + m |= u64(bytes[--i]); + } + return m; + } + +public: + constexpr siphash64(u64 key0, u64 key1, neo::const_buffer buf) noexcept { + u64 v0 = UINT64_C(0x736f6d6570736575); + u64 v1 = UINT64_C(0x646f72616e646f6d); + u64 v2 = UINT64_C(0x6c7967656e657261); + u64 v3 = UINT64_C(0x7465646279746573); + + v0 ^= key0; + v1 ^= key1; + v2 ^= key0; + v3 ^= key1; + + auto siphash_round = [&] { + // Run one SipHash round + v0 += v1; + v1 = std::rotl(v1, 13); + v1 ^= v0; + v0 = std::rotl(v0, 32); + v2 += v3; + v3 = std::rotl(v3, 16); + v3 ^= v2; + v0 += v3; + v3 = std::rotl(v3, 21); + v3 ^= v0; + v2 += v1; + v1 = std::rotl(v1, 17); + v1 ^= v2; + v2 = std::rotl(v2, 32); + }; + + constexpr int c_rounds = 2; + constexpr int d_rounds = 4; + + // Store the low byte of the size in the high bits of `b` + u64 b = (u64)buf.size() << 56; + + while (buf.size() >= 8) { + const auto m = _read_u64_le(buf); + buf += 8; + v3 ^= m; + for (auto i = 0; i < c_rounds; ++i) { + siphash_round(); + } + v0 ^= m; + } + + // Copy the remaining bytes + switch (buf.size()) { + case 7: + b |= static_cast(buf[6]) << 48; + [[fallthrough]]; + case 6: + b |= static_cast(buf[5]) << 40; + [[fallthrough]]; + case 5: + b |= static_cast(buf[4]) << 32; + [[fallthrough]]; + case 4: + b |= static_cast(buf[3]) << 24; + [[fallthrough]]; + case 3: + b |= static_cast(buf[2]) << 16; + [[fallthrough]]; + case 2: + b |= static_cast(buf[1]) << 8; + [[fallthrough]]; + case 1: + b |= static_cast(buf[0]); + break; + case 0: + break; + } + + v3 ^= b; + + for (auto i = 0; i < c_rounds; ++i) { + siphash_round(); + } + + v0 ^= b; + + v2 ^= 0xff; + for (auto i = 0; i < d_rounds; ++i) { + siphash_round(); + } + + _digest = v0 ^ v1 ^ v2 ^ v3; + } + + [[nodiscard]] constexpr std::uint64_t digest() const { return _digest; } +}; + +} // namespace bpt diff --git a/src/bpt/util/siphash.test.cpp b/src/bpt/util/siphash.test.cpp new file mode 100644 index 00000000..56ea2e5e --- /dev/null +++ b/src/bpt/util/siphash.test.cpp @@ -0,0 +1,48 @@ +#include "./siphash.hpp" + +#include + +#include + +const std::uint64_t vectors_sip64[64] = { + {0x310e0edd47db6f72}, {0xfd67dc93c539f874}, {0x5a4fa9d909806c0d}, {0x2d7efbd796666785}, + {0xb7877127e09427cf}, {0x8da699cd64557618}, {0xcee3fe586e46c9cb}, {0x37d1018bf50002ab}, + {0x6224939a79f5f593}, {0xb0e4a90bdf82009e}, {0xf3b9dd94c5bb5d7a}, {0xa7ad6b22462fb3f4}, + {0xfbe50e86bc8f1e75}, {0x903d84c02756ea14}, {0xeef27a8e90ca23f7}, {0xe545be4961ca29a1}, + {0xdb9bc2577fcc2a3f}, {0x9447be2cf5e99a69}, {0x9cd38d96f0b3c14b}, {0xbd6179a71dc96dbb}, + {0x98eea21af25cd6be}, {0xc7673b2eb0cbf2d0}, {0x883ea3e395675393}, {0xc8ce5ccd8c030ca8}, + {0x94af49f6c650adb8}, {0xeab8858ade92e1bc}, {0xf315bb5bb835d817}, {0xadcf6b0763612e2f}, + {0xa5c91da7acaa4dde}, {0x716595876650a2a6}, {0x28ef495c53a387ad}, {0x42c341d8fa92d832}, + {0xce7cf2722f512771}, {0xe37859f94623f3a7}, {0x381205bb1ab0e012}, {0xae97a10fd434e015}, + {0xb4a31508beff4d31}, {0x81396229f0907902}, {0x4d0cf49ee5d4dcca}, {0x5c73336a76d8bf9a}, + {0xd0a704536ba93e0e}, {0x925958fcd6420cad}, {0xa915c29bc8067318}, {0x952b79f3bc0aa6d4}, + {0xf21df2e41d4535f9}, {0x87577519048f53a9}, {0x10a56cf5dfcd9adb}, {0xeb75095ccd986cd0}, + {0x51a9cb9ecba312e6}, {0x96afadfc2ce666c7}, {0x72fe52975a4364ee}, {0x5a1645b276d592a1}, + {0xb274cb8ebf87870a}, {0x6f9bb4203de7b381}, {0xeaecb2a30b22a87f}, {0x9924a43cc1315724}, + {0xbd838d3aafbf8db7}, {0x0b1a2a3265d51aea}, {0x135079a3231ce660}, {0x932b2846e4d70666}, + {0xe1915f5cb1eca46c}, {0xf325965ca16d629f}, {0x575ff28e60381be5}, {0x724506eb4c328a95}, +}; + +template +constexpr I flip_endian(I val) { + I ret = 0; + for (auto i = 0u; i < sizeof(val); ++i) { + std::uint8_t b = val & 0xff; + ret <<= 8; + ret |= b; + val >>= 8; + } + return ret; +} + +TEST_CASE("Calculate some hashes") { + std::array plain; + std::generate(plain.begin(), plain.end(), [v = 0]() mutable { return std::byte(v++); }); + for (std::size_t i = 0; i < 64; ++i) { + auto k0 = UINT64_C(0x0706050403020100); + auto k1 = UINT64_C(0x0f0e0d0c0b0a0908); + auto part = neo::as_buffer(plain, i); + auto digest = bpt::siphash64(k0, k1, part).digest(); + CHECK(digest == flip_endian(vectors_sip64[i])); + } +} diff --git a/src/dds/util/string.hpp b/src/bpt/util/string.hpp similarity index 98% rename from src/dds/util/string.hpp rename to src/bpt/util/string.hpp index 2c369264..ed98e830 100644 --- a/src/dds/util/string.hpp +++ b/src/bpt/util/string.hpp @@ -5,7 +5,7 @@ #include #include -namespace dds { +namespace bpt { inline namespace string_utils { @@ -103,4 +103,4 @@ inline std::string joinstr(std::string_view joiner, Range&& rng) { } // namespace string_utils -} // namespace dds \ No newline at end of file +} // namespace bpt \ No newline at end of file diff --git a/src/dds/util/string.test.cpp b/src/bpt/util/string.test.cpp similarity index 92% rename from src/dds/util/string.test.cpp rename to src/bpt/util/string.test.cpp index 5a4cb2d3..234d1052 100644 --- a/src/dds/util/string.test.cpp +++ b/src/bpt/util/string.test.cpp @@ -1,12 +1,12 @@ -#include +#include #include -using namespace dds; +using namespace bpt; #define CHECK_SPLIT(str, key, ...) \ do { \ - CHECK(dds::split(str, key) == std::vector(__VA_ARGS__)); \ + CHECK(bpt::split(str, key) == std::vector(__VA_ARGS__)); \ } while (0) TEST_CASE("starts_with") { diff --git a/src/dds/util/time.hpp b/src/bpt/util/time.hpp similarity index 97% rename from src/dds/util/time.hpp rename to src/bpt/util/time.hpp index fd03dad2..1c574be6 100644 --- a/src/dds/util/time.hpp +++ b/src/bpt/util/time.hpp @@ -5,7 +5,7 @@ #include #include -namespace dds { +namespace bpt { class stopwatch { public: @@ -44,4 +44,4 @@ auto timed(Func&& fn) noexcept(noexcept(NEO_FWD(fn)())) { } } -} // namespace dds \ No newline at end of file +} // namespace bpt \ No newline at end of file diff --git a/src/bpt/util/tl.hpp b/src/bpt/util/tl.hpp new file mode 100644 index 00000000..c023ed04 --- /dev/null +++ b/src/bpt/util/tl.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include + +#define BPT_CTL(...) \ + (TlArgs && ... _args) \ + ->decltype(auto) requires(NEO_TL_REQUIRES(__VA_ARGS__)) { \ + [[maybe_unused]] auto&& _1 = ::neo::tl_detail::nth_arg<0>(NEO_FWD(_args)...); \ + [[maybe_unused]] auto&& _2 = ::neo::tl_detail::nth_arg<1>(NEO_FWD(_args)...); \ + [[maybe_unused]] auto&& _3 = ::neo::tl_detail::nth_arg<2>(NEO_FWD(_args)...); \ + [[maybe_unused]] auto&& _4 = ::neo::tl_detail::nth_arg<3>(NEO_FWD(_args)...); \ + return (__VA_ARGS__); \ + } + +#define BPT_TL [&] BPT_CTL diff --git a/src/bpt/util/url.cpp b/src/bpt/util/url.cpp new file mode 100644 index 00000000..c6c089f6 --- /dev/null +++ b/src/bpt/util/url.cpp @@ -0,0 +1,54 @@ +#include "./url.hpp" + +#include + +#include + +#include + +using namespace bpt; + +neo::url bpt::parse_url(std::string_view sv) { + BPT_E_SCOPE(e_url_string{std::string(sv)}); + return neo::url::parse(sv); +} + +neo::url bpt::guess_url_from_string(std::string_view sv) { + /// We can probably be a lot smarter about this... + std::filesystem::path as_path{sv}; + if (not as_path.empty() + and (as_path.has_root_path() + or as_path.begin()->filename() == neo::oper::any_of("..", "."))) { + return neo::url::for_file_path(as_path); + } + + if (sv.find("://") == sv.npos) { + std::string s = "https://" + std::string(sv); + auto url = bpt::parse_url(s); + if (url.host == neo::oper::any_of("localhost", "127.0.0.1", "[::1]")) { + url.scheme = "http"; + } + return url; + } + + auto host = neo::url::host_t::parse(sv); + if (host.has_value()) { + std::string s = "https://" + std::string(sv); + return bpt::parse_url(s); + } + + auto parsed = neo::url::try_parse(sv); + if (std::holds_alternative(parsed)) { + return std::get(parsed); + } + + if (as_path.begin() != as_path.end()) { + auto first = as_path.begin()->string(); + host = neo::url::host_t::parse(first); + if (host.has_value()) { + std::string s = "https://" + std::string(sv); + return bpt::parse_url(s); + } + } + return bpt::parse_url(sv); +} diff --git a/src/bpt/util/url.hpp b/src/bpt/util/url.hpp new file mode 100644 index 00000000..f8e4eac3 --- /dev/null +++ b/src/bpt/util/url.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include + +#include + +#include + +namespace bpt { + +struct e_url_string { + std::string value; +}; + +neo::url parse_url(std::string_view sv); + +neo::url guess_url_from_string(std::string_view s); + +} // namespace bpt diff --git a/src/bpt/util/wrap_var.hpp b/src/bpt/util/wrap_var.hpp new file mode 100644 index 00000000..cfeae901 --- /dev/null +++ b/src/bpt/util/wrap_var.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include + +#include +#include + +namespace bpt { + +/** + * @brief Use as a base class to create a type that wraps a variant in an alternative interface + * + * @tparam Ts The variant alternatives that are supported by this type. + */ +template +class variant_wrapper { + using variant_type = std::variant; + + variant_type _var; + +public: + // clang-format off + template + requires std::constructible_from + // Explicit unless the variant can implicitly construct from the argument + explicit(!std::convertible_to) + constexpr variant_wrapper(Arg&& arg) + // Noexcept if the conversion is noexcept + noexcept(std::is_nothrow_constructible_v) + // Construct: + : _var(NEO_FWD(arg)) {} + // clang-format on + + constexpr variant_wrapper() noexcept = default; + + variant_wrapper(const variant_wrapper&) noexcept = default; + variant_wrapper(variant_wrapper&&) noexcept = default; + + variant_wrapper& operator=(const variant_wrapper&) noexcept = default; + variant_wrapper& operator=(variant_wrapper&&) noexcept = default; + +protected: + constexpr decltype(auto) visit(auto&& func) const { return std::visit(func, _var); } + constexpr decltype(auto) visit(auto&& func) { return std::visit(func, _var); } + + /** + * @brief Check whether this wrapper currently holds the named alternative + */ + template + constexpr bool is() const noexcept requires(std::same_as || ...) { + return std::holds_alternative(_var); + } + + /** + * @brief Obtain the named alternative + */ + template + constexpr T& as() & noexcept { + return *std::get_if(&_var); + } + + template + constexpr const T& as() const& noexcept { + return *std::get_if(&_var); + } + + template + constexpr T&& as() && noexcept { + return std::move(*std::get_if(&_var)); + } + + template + constexpr const T&& as() const&& noexcept { + return std::move(*std::get_if(&_var)); + } + + constexpr bool operator==(const variant_wrapper& other) const noexcept { + return _var == other._var; + } + + constexpr auto operator<=>(const variant_wrapper& other) const noexcept { + return _var <=> other._var; + } +}; + +} // namespace bpt \ No newline at end of file diff --git a/src/bpt/util/wrap_var.test.cpp b/src/bpt/util/wrap_var.test.cpp new file mode 100644 index 00000000..bea6aced --- /dev/null +++ b/src/bpt/util/wrap_var.test.cpp @@ -0,0 +1,9 @@ +#include "wrap_var.hpp" + +#include + +struct wrapped : bpt::variant_wrapper { + using variant_wrapper::variant_wrapper; +}; + +static_assert(std::convertible_to); diff --git a/src/bpt/util/yaml/convert.cpp b/src/bpt/util/yaml/convert.cpp new file mode 100644 index 00000000..81ada705 --- /dev/null +++ b/src/bpt/util/yaml/convert.cpp @@ -0,0 +1,64 @@ +#include "./convert.hpp" + +#include "./errors.hpp" + +#include + +#include +#include +#include +#include +#include + +using namespace bpt; + +template +static T try_decode(const YAML::Node& node) { + try { + return node.as(); + } catch (const YAML::TypedBadConversion& exc) { + BOOST_LEAF_THROW_EXCEPTION(e_yaml_invalid_spelling{node.Scalar()}); + } +} + +json5::data bpt::yaml_as_json5_data(const YAML::Node& node) { + std::string_view tag = node.Tag(); + BPT_E_SCOPE(e_yaml_tag{std::string(tag)}); + if (node.IsNull()) { + return json5::data::null_type{}; + } else if (node.IsSequence()) { + auto arr = json5::data::array_type{}; + for (auto& elem : node) { + arr.push_back(yaml_as_json5_data(elem)); + } + return arr; + } else if (node.IsMap()) { + auto obj = json5::data::mapping_type{}; + for (auto& pair : node) { + obj.emplace(pair.first.as(), yaml_as_json5_data(pair.second)); + } + return obj; + } else if (tag == neo::oper::any_of("!", "tag:yaml.org,2002:str")) { + return node.as(); + } else if (tag == "?") { + auto spell = node.Scalar(); + if (spell == neo::oper::any_of("true", "false")) { + return node.as(); + } + try { + return node.as(); + } catch (const YAML::TypedBadConversion&) { + return spell; + } + } else if (tag == "tag:yaml.org,2002:bool") { + return try_decode(node); + } else if (tag == "tag:yaml.org,2002:null") { + return nullptr; + } else if (tag == "tag:yaml.org,2002:int") { + return static_cast(try_decode(node)); + } else if (tag == "tag:yaml.org,2002:float") { + return try_decode(node); + } else { + BOOST_LEAF_THROW_EXCEPTION(e_yaml_unknown_tag{node.Tag()}); + } +} diff --git a/src/bpt/util/yaml/convert.hpp b/src/bpt/util/yaml/convert.hpp new file mode 100644 index 00000000..046c3ac7 --- /dev/null +++ b/src/bpt/util/yaml/convert.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +#include + +namespace bpt { + +json5::data yaml_as_json5_data(const YAML::Node&); + +} // namespace bpt \ No newline at end of file diff --git a/src/bpt/util/yaml/convert.test.cpp b/src/bpt/util/yaml/convert.test.cpp new file mode 100644 index 00000000..cd873cd6 --- /dev/null +++ b/src/bpt/util/yaml/convert.test.cpp @@ -0,0 +1,36 @@ +#include "./convert.hpp" +#include "./parse.hpp" + +#include "./errors.hpp" + +#include +#include + +#include + +static json5::data parse_and_convert(std::string_view data) { + auto node = bpt::parse_yaml_string(data); + return bpt::yaml_as_json5_data(node); +} + +TEST_CASE("Convert some simple scalars") { + auto [given, expect] = GENERATE(Catch::Generators::table({ + {"null", json5::data::null_type{}}, + {"", json5::data::null_type{}}, + {"false", false}, + // No "no" as bool: + {"no", "no"}, + // Unless they ask for it + {"!!bool no", false}, + {"!!str false", "false"}, + {"'false'", "false"}, + {"\"false\"", "false"}, + {"!!null lol", nullptr}, + {"!!int 4", 4.0}, + {"!!float 4", 4.0}, + })); + + CAPTURE(given); + auto data = REQUIRES_LEAF_NOFAIL(parse_and_convert(given)); + CHECK(data == expect); +} diff --git a/src/bpt/util/yaml/errors.hpp b/src/bpt/util/yaml/errors.hpp new file mode 100644 index 00000000..6247b33f --- /dev/null +++ b/src/bpt/util/yaml/errors.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +namespace bpt { + +struct e_parse_yaml_file_path { + std::filesystem::path value; +}; + +struct e_parse_yaml_string { + std::string value; +}; + +struct e_yaml_parse_error { + std::string value; +}; + +struct e_yaml_unknown_tag { + std::string value; +}; + +struct e_yaml_tag { + std::string value; +}; + +struct e_yaml_invalid_spelling { + std::string value; +}; + +} // namespace bpt \ No newline at end of file diff --git a/src/bpt/util/yaml/parse.cpp b/src/bpt/util/yaml/parse.cpp new file mode 100644 index 00000000..bc99312e --- /dev/null +++ b/src/bpt/util/yaml/parse.cpp @@ -0,0 +1,29 @@ +#include "./parse.hpp" + +#include "./errors.hpp" + +#include +#include + +#include +#include +#include + +#include + +using namespace bpt; + +YAML::Node bpt::parse_yaml_file(const std::filesystem::path& fpath) { + BPT_E_SCOPE(e_parse_yaml_file_path{fpath}); + auto content = bpt::read_file(fpath); + return parse_yaml_string(content); +} + +YAML::Node bpt::parse_yaml_string(std::string_view sv) { + BPT_E_SCOPE(e_parse_yaml_string{std::string(sv)}); + try { + return YAML::Load(sv.data()); + } catch (YAML::Exception const& exc) { + BOOST_LEAF_THROW_EXCEPTION(exc, e_yaml_parse_error{exc.what()}); + } +} \ No newline at end of file diff --git a/src/bpt/util/yaml/parse.hpp b/src/bpt/util/yaml/parse.hpp new file mode 100644 index 00000000..f2a2f9dd --- /dev/null +++ b/src/bpt/util/yaml/parse.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include + +#include +#include + +namespace bpt { + +YAML::Node parse_yaml_file(const std::filesystem::path&); +YAML::Node parse_yaml_string(std::string_view); + +} // namespace bpt \ No newline at end of file diff --git a/src/dds/build/builder.cpp b/src/dds/build/builder.cpp deleted file mode 100644 index 39855019..00000000 --- a/src/dds/build/builder.cpp +++ /dev/null @@ -1,393 +0,0 @@ -#include "./builder.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include -#include - -using namespace dds; -using namespace fansi::literals; - -namespace { - -struct state { - bool generate_catch2_header = false; - bool generate_catch2_main = false; -}; - -void log_failure(const test_failure& fail) { - dds_log(error, - "Test .br.yellow[{}] .br.red[{}] [Exited {}]"_styled, - fail.executable_path.string(), - fail.timed_out ? "TIMED OUT" : "FAILED", - fail.retc); - if (fail.signal) { - dds_log(error, "Test execution received signal {}", fail.signal); - } - if (trim_view(fail.output).empty()) { - dds_log(error, "(Test executable produced no output)"); - } else { - dds_log(error, "Test output:\n{}[dds - test output end]", fail.output); - } -} - -lm::library -prepare_catch2_driver(test_lib test_driver, const build_params& params, build_env_ref env_) { - fs::path test_include_root = params.out_root / "_catch-2.10.2"; - - lm::library ret_lib; - auto catch_hpp = test_include_root / "catch2/catch.hpp"; - if (!fs::exists(catch_hpp)) { - fs::create_directories(catch_hpp.parent_path()); - auto hpp_strm = open(catch_hpp, std::ios::out | std::ios::binary); - auto c2_str = detail::catch2_embedded_single_header_str(); - hpp_strm.write(c2_str.data(), c2_str.size()); - hpp_strm.close(); - } - ret_lib.include_paths.push_back(test_include_root); - - if (test_driver == test_lib::catch_) { - // Don't compile a library helper - return ret_lib; - } - - std::string fname; - std::string definition; - - if (test_driver == test_lib::catch_main) { - fname = "catch-main.cpp"; - definition = "CATCH_CONFIG_MAIN"; - } else { - assert(false && "Impossible: Invalid `test_driver` for catch library"); - std::terminate(); - } - - shared_compile_file_rules comp_rules; - comp_rules.defs().push_back(definition); - - auto catch_cpp = test_include_root / "catch2" / fname; - auto cpp_strm = open(catch_cpp, std::ios::out | std::ios::binary); - cpp_strm << "#include \"./catch.hpp\"\n"; - cpp_strm.close(); - - auto sf = source_file::from_path(catch_cpp, test_include_root); - assert(sf.has_value()); - - compile_file_plan plan{comp_rules, std::move(*sf), "Catch2", "v1"}; - - build_env env2 = env_; - env2.output_root /= "_test-driver"; - auto obj_file = plan.calc_object_file_path(env2); - - if (!fs::exists(obj_file)) { - dds_log(info, "Compiling Catch2 test driver (This will only happen once)..."); - compile_all(std::array{plan}, env2, 1); - } - - ret_lib.linkable_path = obj_file; - return ret_lib; -} - -lm::library -prepare_test_driver(const build_params& params, test_lib test_driver, build_env_ref env) { - if (test_driver == test_lib::catch_ || test_driver == test_lib::catch_main) { - return prepare_catch2_driver(test_driver, params, env); - } else { - assert(false && "Unreachable"); - std::terminate(); - } -} - -library_plan prepare_library(state& st, - const sdist_target& sdt, - const library_root& lib, - const package_manifest& pkg_man) { - library_build_params lp; - lp.out_subdir = sdt.params.subdir; - lp.build_apps = sdt.params.build_apps; - lp.build_tests = sdt.params.build_tests; - lp.enable_warnings = sdt.params.enable_warnings; - if (lp.build_tests) { - if (pkg_man.test_driver == test_lib::catch_ - || pkg_man.test_driver == test_lib::catch_main) { - lp.test_uses.push_back({".dds", "Catch"}); - st.generate_catch2_header = true; - if (pkg_man.test_driver == test_lib::catch_main) { - lp.test_uses.push_back({".dds", "Catch-Main"}); - st.generate_catch2_main = true; - } - } - } - return library_plan::create(lib, std::move(lp), pkg_man.namespace_ + "/" + lib.manifest().name); -} - -package_plan prepare_one(state& st, const sdist_target& sd) { - package_plan pkg{sd.sd.manifest.id.name, sd.sd.manifest.namespace_}; - auto libs = collect_libraries(sd.sd.path); - for (const auto& lib : libs) { - pkg.add_library(prepare_library(st, sd, lib, sd.sd.manifest)); - } - return pkg; -} - -build_plan prepare_build_plan(state& st, const std::vector& sdists) { - build_plan plan; - for (const auto& sd_target : sdists) { - plan.add_package(prepare_one(st, sd_target)); - } - return plan; -} - -usage_requirement_map -prepare_ureqs(const build_plan& plan, const toolchain& toolchain, path_ref out_root) { - usage_requirement_map ureqs; - for (const auto& pkg : plan.packages()) { - for (const auto& lib : pkg.libraries()) { - auto& lib_reqs = ureqs.add(pkg.namespace_(), lib.name()); - lib_reqs.include_paths.push_back(lib.library_().public_include_dir()); - lib_reqs.uses = lib.library_().manifest().uses; - lib_reqs.links = lib.library_().manifest().links; - if (const auto& arc = lib.archive_plan()) { - lib_reqs.linkable_path = out_root / arc->calc_archive_file_path(toolchain); - } - auto gen_incdir_opt = lib.generated_include_dir(); - if (gen_incdir_opt) { - lib_reqs.include_paths.push_back(out_root / *gen_incdir_opt); - } - } - } - return ureqs; -} - -void write_lml(build_env_ref env, const library_plan& lib, path_ref lml_path) { - fs::create_directories(lml_path.parent_path()); - auto out = open(lml_path, std::ios::binary | std::ios::out); - out << "Type: Library\n" - << "Name: " << lib.name() << '\n' - << "Include-Path: " << lib.library_().public_include_dir().generic_string() << '\n'; - for (auto&& use : lib.uses()) { - out << "Uses: " << use.namespace_ << "/" << use.name << '\n'; - } - for (auto&& link : lib.links()) { - out << "Links: " << link.namespace_ << "/" << link.name << '\n'; - } - if (auto&& arc = lib.archive_plan()) { - out << "Path: " - << (env.output_root / arc->calc_archive_file_path(env.toolchain)).generic_string() - << '\n'; - } -} - -void write_lmp(build_env_ref env, const package_plan& pkg, path_ref lmp_path) { - fs::create_directories(lmp_path.parent_path()); - auto out = open(lmp_path, std::ios::binary | std::ios::out); - out << "Type: Package\n" - << "Name: " << pkg.name() << '\n' - << "Namespace: " << pkg.namespace_() << '\n'; - for (const auto& lib : pkg.libraries()) { - auto lml_path = lmp_path.parent_path() / pkg.namespace_() / (lib.name() + ".lml"); - write_lml(env, lib, lml_path); - out << "Library: " << lml_path.generic_string() << '\n'; - } -} - -void write_lmi(build_env_ref env, const build_plan& plan, path_ref base_dir, path_ref lmi_path) { - fs::create_directories(fs::absolute(lmi_path).parent_path()); - auto out = open(lmi_path, std::ios::binary | std::ios::out); - out << "Type: Index\n"; - for (const auto& pkg : plan.packages()) { - auto lmp_path = base_dir / "_libman" / (pkg.name() + ".lmp"); - write_lmp(env, pkg, lmp_path); - out << "Package: " << pkg.name() << "; " << lmp_path.generic_string() << '\n'; - } -} - -void write_lib_cmake(build_env_ref env, - std::ostream& out, - const package_plan& pkg, - const library_plan& lib) { - fmt::print(out, "# Library {}/{}\n", pkg.namespace_(), lib.name()); - auto cmake_name = fmt::format("{}::{}", pkg.namespace_(), lib.name()); - auto cm_kind = lib.archive_plan().has_value() ? "STATIC" : "INTERFACE"; - fmt::print( - out, - "if(TARGET {0})\n" - " get_target_property(dds_imported {0} dds_IMPORTED)\n" - " if(NOT dds_imported)\n" - " message(WARNING [[A target \"{0}\" is already defined, and not by a dds import]])\n" - " endif()\n" - "else()\n", - cmake_name); - fmt::print(out, - " add_library({0} {1} IMPORTED GLOBAL)\n" - " set_property(TARGET {0} PROPERTY dds_IMPORTED TRUE)\n" - " set_property(TARGET {0} PROPERTY INTERFACE_INCLUDE_DIRECTORIES [[{2}]])\n", - cmake_name, - cm_kind, - lib.library_().public_include_dir().generic_string()); - for (auto&& use : lib.uses()) { - fmt::print(out, - " set_property(TARGET {} APPEND PROPERTY INTERFACE_LINK_LIBRARIES {}::{})\n", - cmake_name, - use.namespace_, - use.name); - } - for (auto&& link : lib.links()) { - fmt::print(out, - " set_property(TARGET {} APPEND PROPERTY\n" - " INTERFACE_LINK_LIBRARIES $)\n", - cmake_name, - link.namespace_, - link.name); - } - if (auto& arc = lib.archive_plan()) { - fmt::print(out, - " set_property(TARGET {} PROPERTY IMPORTED_LOCATION [[{}]])\n", - cmake_name, - (env.output_root / arc->calc_archive_file_path(env.toolchain)).generic_string()); - } - fmt::print(out, "endif()\n"); -} - -void write_cmake_pkg(build_env_ref env, std::ostream& out, const package_plan& pkg) { - fmt::print(out, "## Imports for {}\n", pkg.name()); - for (auto& lib : pkg.libraries()) { - write_lib_cmake(env, out, pkg, lib); - } - fmt::print(out, "\n"); -} - -void write_cmake(build_env_ref env, const build_plan& plan, path_ref cmake_out) { - fs::create_directories(fs::absolute(cmake_out).parent_path()); - auto out = open(cmake_out, std::ios::binary | std::ios::out); - out << "## This CMake file was generated by `dds build-deps`. DO NOT EDIT!\n\n"; - for (const auto& pkg : plan.packages()) { - write_cmake_pkg(env, out, pkg); - } -} - -/** - * @brief Calculate a hash of the directory layout of the given directory. - * - * Because a tweaks-dir is specifically designed to have files added/removed within it, and - * its contents are inspected by `__has_include`, we need to have a way to invalidate any caches - * when the content of that directory changes. We don't care to hash the contents of the files, - * since those will already break any caches. - */ -std::string hash_tweaks_dir(const fs::path& tweaks_dir) { - if (!fs::is_directory(tweaks_dir)) { - return "0"; // No tweaks directory, no cache to bust - } - std::vector children{fs::recursive_directory_iterator{tweaks_dir}, - fs::recursive_directory_iterator{}}; - std::sort(children.begin(), children.end()); - // A really simple inline djb2 hash - std::uint32_t hash = 5381; - for (auto& p : children) { - for (std::uint32_t c : fs::weakly_canonical(p).string()) { - hash = ((hash << 5) + hash) + c; - } - } - return std::to_string(hash); -} - -template -void with_build_plan(const build_params& params, - const std::vector& sdists, - Func&& fn) { - fs::create_directories(params.out_root); - auto db = database::open(params.out_root / ".dds.db"); - - state st; - auto plan = prepare_build_plan(st, sdists); - auto ureqs = prepare_ureqs(plan, params.toolchain, params.out_root); - build_env env{ - params.toolchain, - params.out_root, - db, - toolchain_knobs{ - .is_tty = stdout_is_a_tty(), - .tweaks_dir = params.tweaks_dir, - }, - ureqs, - }; - - if (env.knobs.tweaks_dir) { - env.knobs.cache_buster = hash_tweaks_dir(*env.knobs.tweaks_dir); - dds_log(trace, - "Build cache-buster value for tweaks-dir [{}] content is '{}'", - *env.knobs.tweaks_dir, - *env.knobs.cache_buster); - } - - if (st.generate_catch2_main) { - auto catch_lib = prepare_test_driver(params, test_lib::catch_main, env); - ureqs.add(".dds", "Catch-Main") = catch_lib; - } - if (st.generate_catch2_header) { - auto catch_lib = prepare_test_driver(params, test_lib::catch_, env); - ureqs.add(".dds", "Catch") = catch_lib; - } - - if (params.generate_compdb) { - generate_compdb(plan, env); - } - - plan.render_all(env); - - fn(std::move(env), std::move(plan)); -} - -} // namespace - -void builder::compile_files(const std::vector& files, const build_params& params) const { - with_build_plan(params, _sdists, [&](build_env_ref env, build_plan plan) { - plan.compile_files(env, params.parallel_jobs, files); - }); -} - -void builder::build(const build_params& params) const { - with_build_plan(params, _sdists, [&](build_env_ref env, const build_plan& plan) { - dds::stopwatch sw; - plan.compile_all(env, params.parallel_jobs); - dds_log(info, "Compilation completed in {:L}ms", sw.elapsed_ms().count()); - - sw.reset(); - plan.archive_all(env, params.parallel_jobs); - dds_log(info, "Archiving completed in {:L}ms", sw.elapsed_ms().count()); - - sw.reset(); - plan.link_all(env, params.parallel_jobs); - dds_log(info, "Runtime binary linking completed in {:L}ms", sw.elapsed_ms().count()); - - sw.reset(); - auto test_failures = plan.run_all_tests(env, params.parallel_jobs); - dds_log(info, "Test execution finished in {:L}ms", sw.elapsed_ms().count()); - - for (auto& fail : test_failures) { - log_failure(fail); - } - if (!test_failures.empty()) { - throw_user_error(); - } - - if (params.emit_lmi) { - write_lmi(env, plan, params.out_root, *params.emit_lmi); - } - - if (params.emit_cmake) { - write_cmake(env, plan, *params.emit_cmake); - } - }); -} diff --git a/src/dds/build/plan/base.hpp b/src/dds/build/plan/base.hpp deleted file mode 100644 index 89071c65..00000000 --- a/src/dds/build/plan/base.hpp +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -namespace dds { - -struct build_env { - dds::toolchain toolchain; - fs::path output_root; - database& db; - - toolchain_knobs knobs; - - const usage_requirement_map& ureqs; -}; - -using build_env_ref = const build_env&; - -} // namespace dds diff --git a/src/dds/build/plan/library.cpp b/src/dds/build/plan/library.cpp deleted file mode 100644 index aa461a4f..00000000 --- a/src/dds/build/plan/library.cpp +++ /dev/null @@ -1,152 +0,0 @@ -#include "./library.hpp" - -#include -#include - -#include -#include -#include -#include - -#include -#include - -using namespace dds; - -namespace { - -const std::string gen_dir_qual = "__dds/gen"; - -fs::path rebase_gen_incdir(path_ref subdir) { return gen_dir_qual / subdir; } -} // namespace - -std::optional library_plan::generated_include_dir() const noexcept { - if (_templates.empty()) { - return std::nullopt; - } - return rebase_gen_incdir(output_subdirectory()); -} - -library_plan library_plan::create(const library_root& lib, - const library_build_params& params, - std::optional qual_name_) { - // Source files are kept in three groups: - std::vector app_sources; - std::vector test_sources; - std::vector lib_sources; - std::vector template_sources; - - auto qual_name = std::string(qual_name_.value_or(lib.manifest().name)); - - // Collect the source for this library. This will look for any compilable sources in the - // `src/` subdirectory of the library. - auto src_dir = lib.src_source_root(); - if (src_dir.exists()) { - // Sort each source file between the three source arrays, depending on - // the kind of source that we are looking at. - auto all_sources = src_dir.collect_sources(); - for (const auto& sfile : all_sources) { - if (sfile.kind == source_kind::test) { - test_sources.push_back(sfile); - } else if (sfile.kind == source_kind::app) { - app_sources.push_back(sfile); - } else if (sfile.kind == source_kind::source) { - lib_sources.push_back(sfile); - } else if (sfile.kind == source_kind::header_template) { - template_sources.push_back(sfile); - } else { - assert(sfile.kind == source_kind::header); - } - } - } - - // Load up the compile rules - auto compile_rules = lib.base_compile_rules(); - compile_rules.enable_warnings() = params.enable_warnings; - compile_rules.uses() = lib.manifest().uses; - - const auto codegen_subdir = rebase_gen_incdir(params.out_subdir); - - if (!template_sources.empty()) { - compile_rules.include_dirs().push_back(codegen_subdir); - } - - // Convert the library sources into their respective file compilation plans. - auto lib_compile_files = // - lib_sources // - | ranges::views::transform([&](const source_file& sf) { - return compile_file_plan(compile_rules, sf, qual_name, params.out_subdir / "obj"); - }) - | ranges::to_vector; - - // If we have any compiled library files, generate a static library archive - // for this library - std::optional archive_plan; - if (!lib_compile_files.empty()) { - dds_log(debug, "Generating an archive library for {}", qual_name); - archive_plan.emplace(lib.manifest().name, - qual_name, - params.out_subdir, - std::move(lib_compile_files)); - } else { - dds_log(debug, - "Library {} has no compiled inputs, so no archive will be generated", - qual_name); - } - - // Collect the paths to linker inputs that should be used when generating executables for this - // library. - std::vector links; - extend(links, lib.manifest().uses); - extend(links, lib.manifest().links); - - // There may also be additional usage requirements for tests - auto test_rules = compile_rules.clone(); - auto test_links = links; - extend(test_rules.uses(), params.test_uses); - extend(test_links, params.test_uses); - - // Generate the plans to link any executables for this library - std::vector link_executables; - for (const source_file& source : ranges::views::concat(app_sources, test_sources)) { - const bool is_test = source.kind == source_kind::test; - if (is_test && !params.build_tests) { - // This is a test, but we don't want to build tests - continue; - } - if (!is_test && !params.build_apps) { - // This is an app, but we don't want to build apps - continue; - } - // Pick a subdir based on app/test - const auto subdir_base = is_test ? params.out_subdir / "test" : params.out_subdir; - // Put test/app executables in a further subdirectory based on the source file path - const auto subdir = subdir_base / source.relative_path().parent_path(); - // Pick compile rules based on app/test - auto rules = is_test ? test_rules : compile_rules; - // Pick input libs based on app/test - auto& exe_links = is_test ? test_links : links; - // TODO: Apps/tests should only see the _public_ include dir, not both - auto exe = link_executable_plan{exe_links, - compile_file_plan(rules, - source, - qual_name, - params.out_subdir / "obj"), - subdir, - source.path.stem().stem().string()}; - link_executables.emplace_back(std::move(exe)); - } - - std::vector render_templates; - for (const auto& sf : template_sources) { - render_templates.emplace_back(sf, codegen_subdir); - } - - // Done! - return library_plan{lib, - qual_name, - params.out_subdir, - std::move(archive_plan), - std::move(link_executables), - std::move(render_templates)}; -} diff --git a/src/dds/build/plan/template.cpp b/src/dds/build/plan/template.cpp deleted file mode 100644 index 9792f2b7..00000000 --- a/src/dds/build/plan/template.cpp +++ /dev/null @@ -1,193 +0,0 @@ -#include - -#include -#include -#include -#include - -#include -#include - -#include -#include - -using namespace dds; - -using json_data = semester::json_data; - -namespace { - -static constexpr ctll::fixed_string IDENT_RE{"([_a-zA-Z]\\w*)(.*)"}; - -std::string_view skip(std::string_view in) { - auto nspace_pos = in.find_first_not_of(" \t\n\r\f"); - in = in.substr(nspace_pos); - if (starts_with(in, "/*")) { - // It's a block comment. Find the block-end marker. - auto block_end = in.find("*/"); - if (block_end == in.npos) { - throw_user_error("Unterminated block comment"); - } - in = in.substr(block_end + 2); - // Recursively skip some more - return skip(in); - } - if (starts_with(in, "//")) { - more: - // It's a line comment. Find the next not-continued newline - auto cn_nl = in.find("\\\n"); - auto nl = in.find("\n"); - if (cn_nl < nl) { - // The next newline is a continuation of the comment. Keep looking - in = in.substr(nl + 1); - goto more; - } - if (nl == in.npos) { - // We've reached the end. Okay. - return in.substr(nl); - } - } - // Not a comment, and not whitespace. Okay. - return in; -} - -std::string stringify(const json_data& dat) { - if (dat.is_bool()) { - return dat.as_bool() ? "true" : "false"; - } else if (dat.is_double()) { - return std::to_string(dat.as_double()); - } else if (dat.is_null()) { - return "nullptr"; - } else if (dat.is_string()) { - /// XXX: This probably isn't quite enough sanitization for edge cases. - auto str = dat.as_string(); - str = replace(str, "\n", "\\n"); - str = replace(str, "\"", "\\\""); - return "\"" + str + "\""; - } else { - throw_user_error("Cannot render un-stringable data type"); - } -} - -std::pair eval_expr_tail(std::string_view in, const json_data& dat) { - in = skip(in); - if (starts_with(in, ".")) { - // Accessing a subproperty of the data - in.remove_prefix(1); - in = skip(in); - // We _must_ see an identifier - auto [is_ident, ident, tail] = ctre::match(in); - if (!is_ident) { - throw_user_error("Expected identifier following dot `.`"); - } - if (!dat.is_mapping()) { - throw_user_error("Cannot use dot `.` on non-mapping object"); - } - auto& map = dat.as_mapping(); - auto found = map.find(ident.to_view()); - if (found == map.end()) { - throw_user_error("No subproperty '{}'", ident.to_view()); - } - return eval_expr_tail(tail, found->second); - } - return {stringify(dat), in}; -} - -std::pair eval_primary_expr(std::string_view in, - const json_data& dat) { - in = skip(in); - - if (in.empty()) { - throw_user_error("Expected primary expression"); - } - - if (in.front() == '(') { - in = in.substr(1); - auto [ret, tail] = eval_primary_expr(in, dat); - if (!starts_with(tail, ")")) { - throw_user_error( - "Expected closing parenthesis `)` following expression"); - } - return {ret, tail.substr(1)}; - } - - auto [is_ident, ident, tail_1] = ctre::match(in); - - if (is_ident) { - auto& map = dat.as_mapping(); - auto found = map.find(ident.to_view()); - if (found == map.end()) { - throw_user_error("Unknown top-level identifier '{}'", - ident.to_view()); - } - - return eval_expr_tail(tail_1, found->second); - } - - return {"nope", in}; -} - -std::string render_template(std::string_view tmpl, const library_root& lib) { - std::string acc; - std::string_view MARKER_STRING = "__dds"; - - // Fill out a data structure that will be exposed to the template - json_data dat = json_data::mapping_type({ - { - "lib", - json_data::mapping_type{ - {"name", lib.manifest().name}, - {"root", lib.path().string()}, - }, - }, - }); - - while (!tmpl.empty()) { - // Find the next marker in the template string - auto next_marker = tmpl.find(MARKER_STRING); - if (next_marker == tmpl.npos) { - // We've reached the end of the template. Stop - acc.append(tmpl); - break; - } - // Append the string up to the next marker - acc.append(tmpl.substr(0, next_marker)); - // Consume up to the next marker - tmpl = tmpl.substr(next_marker + MARKER_STRING.size()); - auto next_not_space = tmpl.find_first_not_of(" \t"); - if (next_not_space == tmpl.npos || tmpl[next_not_space] != '(') { - throw_user_error( - "Expected `(` following `__dds` identifier in template file"); - } - - auto [inner, tail] = eval_primary_expr(tmpl, dat); - acc.append(inner); - tmpl = tail; - } - - return acc; -} - -} // namespace - -void render_template_plan::render(build_env_ref env, const library_root& lib) const { - auto content = slurp_file(_source.path); - - // Calculate the destination of the template rendering - auto dest = env.output_root / _subdir / _source.relative_path(); - dest.replace_filename(dest.stem().stem().filename().string() + dest.extension().string()); - fs::create_directories(dest.parent_path()); - - auto result = render_template(content, lib); - if (fs::is_regular_file(dest)) { - auto existing_content = slurp_file(dest); - if (result == existing_content) { - /// The content of the file has not changed. Do not write a file. - return; - } - } - - auto ofile = open(dest, std::ios::binary | std::ios::out); - ofile << result; - ofile.close(); // Throw any exceptions while closing the file -} diff --git a/src/dds/build/plan/template.hpp b/src/dds/build/plan/template.hpp deleted file mode 100644 index 303ca071..00000000 --- a/src/dds/build/plan/template.hpp +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include -#include - -#include - -namespace dds { - -class library_root; - -class render_template_plan { - /** - * The source file that defines the config template - */ - source_file _source; - /** - * The subdirectory in which the template should be rendered. - */ - fs::path _subdir; - -public: - /** - * Create a new instance - * @param sf The source file of the template - * @param subdir The subdirectort into which the template should render - */ - render_template_plan(source_file sf, path_ref subdir) - : _source(std::move(sf)) - , _subdir(subdir) {} - - /** - * Render the template into its output directory - */ - void render(build_env_ref, const library_root& owning_library) const; -}; - -} // namespace dds \ No newline at end of file diff --git a/src/dds/catch2_embedded.generated.cpp b/src/dds/catch2_embedded.generated.cpp deleted file mode 100644 index 610bcaea..00000000 --- a/src/dds/catch2_embedded.generated.cpp +++ /dev/null @@ -1,87 +0,0 @@ - -#include "./catch2_embedded.hpp" - -#include -#include - -using namespace neo::literals; - -namespace dds::detail { - -static const neo::const_buffer catch2_gzip_bufs[] - = { - "\037\213\010\000\326j\352_\002\377\354}\177\177\333\266\361\360\377~\025\210\363\231K%\262c']\333\331\216\276\217\342(\2117G\366$\271i\327u,-Q6\027\211\324H\312\216\347z\257\375\271\303/\002\040H\221\262\234&m\334:\226H\340p8\034\016w\207\303\341\311\2435\362\210\220\003/\035^\220\313\247[;\317\266\236\321'\257\375\320\217\275\324\037\355\222\247\333O\2677w\2667\237\355\220\235\357v\237n\357>\333\331\332\376\372\317O\277\375\232\226\334\\\372\207V\037\\\004\011\031\007\023\237\\x\0119\363\375\220L\375\370\334\037\221q\034M\311t>I\203\031\276\365\275\221\037'[\344d\342{\211OFQ\370UJ\374Q\220\022\370\177\024\304\3760\235\\\263\336D\263\35388\277H\2113lP\364\311\340*\"/&s\237\034\314\317\374\204\034\245\243-\322\236L\010-\226\220\330O\374\370\322\037mA}\012\342e\220\244qp6\007\002\220y\010\015\223\364\302'/\242(II?\032\247W^\354\223\243`\350\207\211\337$\337\003bA\024\022\240\313\026q\372\276O\274\3410\232\316\274\360:\010\317)@\332\303\243\303\203N\267\337qw\334\355\255\364CJ\242\230\014\001W\342\245\344\"Mg\273O\236\\]]m\235a+[Q|\376\304(\337\000HO\326\036\006c@hL\006\357\216_\034\235v\016N_t\372n\377\260\373\372\250\343\036v\017\216N_v\334\203\366\340\340\215\373\346\344D\365\010}F\222\353$\365\247.\343\210\265\207\376$\030\023\006t\004\365^wO\017\214j\257\017\016r\225\302Q0^3\272\347&\363\331\014x!qa|C\030\272d\353\242\000#\361\354\020@\003\220`8\033r\034\022\312*\2624\231z\3038b\350d\030q\370\316l\236\\4\212^\216\202\304;\233\370\273d\347\033\230z\337|\367\224\225\364'\300\376\320&m\334\254\313(4\012\274\363\020\270(\030\022laa\241\000>\304@\275\365\315w3o4\362G\353u\252$W\001\020o\323\017\347\323Z\365\206\321\245\017\0377y}\040\237\007\263\235\203`\003T0\270\264\005\040\301\013\177\350\315\201\032\275\316\337O\017{\035\040=L\350s\230\2550\342_%\004{\023\373!\214G\342'M\342\205#\0200\254\006\3402\231H88\356Q\214\223\375\222O\345hL\316\037?\006\231t\351\223(\234\\\223\263\371\371\371\265(H\3060u\335\023\332\305\244)\301\250\315\261\252i\004-\022\301V\000\377|\022\235y\223\311\365V\216A\213\206CB\\\307&P\270<\374\346\333\257)\006#?\365\202I\262V\006+\033\377\205m\315C\240\314h\363\322\213\003\344\273\365\312(r\216aC\006H\302\207\222\011\3650\033Q\207\011\201\203\343\356\253\303\327\040@\016\273\015\362\353\257\366\267\275\323n\267\323\243S@\223\040\207oO\216r\017y\235\366\321\021H\226\336\240\257N\370\303\220\316\320`:\233P\261\334$W>\314\2710\305\321\242\243\006\322\033p\306\2570T\004\206\000\226\010`\010\254\305\227!\204s\340\205\360>\241#\214\204\303\362\211\027\372\223\214MN\016\336$\305\375\225\3305\212\360\357\3740\350\364\272\355#\220\307\360\341U\373\240\323\307\242E\000_\036\366\333/@\236\277\305\207\040X\271d\231\323\365\242\264$\026\3443\216\202\177`\205\337\351\322J\007oz\307\335c\267?\350\301\362\361\266\3757>*\005](\252\2434\311\307\346aa\3038\306\356q\367\350\307\206)\261g\023/\205\2510\325\004u\373\344\004\232DA\035\204\303\311|\344\223\375\201\007:Ez\020AK)\314o\030\267\255\213\026\026\200\205\264\335{\335\031\270\307}\370\377\007\362\3749\331\311\015\307\311Q{\360\352\270\367\026\350\205\022\227\012\245\254\332\341\311\233\343ngAMV\010+\363\316\252\222\315\231\004\341\374\203\306\373\256[\370\314u\033\305\355\034\035vO\1770\301\277;\354>{j\200\242\317\000\224\366\324R\360m\377\000Wi\2436,\347\257\337\321\372\305\250\000\260\227\307\357\264\271\227I\006\313\300\351\363\231\2539\007G\355^[\225\020:\253\231o\335\356\361\300}\331yu\330E\235\247\254d\236\377\014\326\2029\035\273A\230\372\361\330\003y\200\210\206\336\324Of\360\215k\3147kl~%\040\016A\000@a\022\207\347\040\243GNco\355\326\350r\036\240\321b\352\235\273\336$\360\022\327\233\247\040^\317Q\013\365bl\331(\011\332\3454\012\355/f\040\323bw\350\315\274\263`\002\334\316P\207r/\375\024\224d\342\021X\250\317`\221\004\221&\212\223\261\357\245s\220\324d\223\234]\313\307Xk\000Ro\034M&\321\025\310\357\254\034*\300\234\035v\241\030\025\210\352\224=8>E\231EvI\0404\"\376H\350D\331\252\373\177\271\332\234u\334~\347\015\203\360.\010G\321UB\360AI\275\223\343\376\341\017\240\326\276\006\241\331\207\232\210%}Fp\210`\326\227U\026B\261\363\303A\347d\000Z)Bh\003\004\377\303\320\237\241\330H`4qid\225\037\031?\370\254\033\245t\325\237z0\320\360\013k\305.\012\231\320\277\202\307\347\347\023N:\272f\222\231\260e\206\363\251\217\013\320\205?E(A\010C\020\216\203\3639\330`\320\356\326t\324\204\352\221\265U\276\246\235S\213mB|\017\030\223\021\030-*\217\300\224p\367\371\270\021\344\340\026*\017\264\035\307\337:\337\322\211\000\2055\"6\310\325E\000\020\2712\312\306\222C\333B\030o\301\312\221l\321D[f\026\341D\210P\374\040\303\001\376M1\013a\311dk\031.j\014\313&\201%\024\200^#\260!\024\200\345\024\370\357\0149\253\011\364\336\004\025\013\340\202\216x\301\370\0100\244\035H\010\210\020?\336R5\364\331d\236\340\357\032_\307\034\345\031i=\007cp\347\353\355\247GT\2309\212\210\373\376\300=jw_7\310\306\006\311\276\2525l+\334\311\311\316\327\356q\317}\335\353\264\007\352\212V\334\372\267\333\317j\266Nk\024\264\376mA\353\231@{\347KE\324\273\214\002P\200#\240#Z-\250\015\037\240v\336\224J1\216\002\250\320\327\214\201\223\367\224\037\023\177r\351S\205\007\270\351|8dz\264P\231\250n\214\272a\200Z\025L\324\263I4|\257\351\032\227BSw\310\272E9^'\013\300\036\237\330\241\226\200\215f\010\265\014,L\256\343^\307}q\312Pv\266\266\266\032\304\301\021j\270\356\331<\230\244A\010\302<\004\331\036\246\356\014\210\360}\333\005\215\243\217T\3204\246\220\014\232\376\245\234\301\177\301E<\214\322l\040\321\333f\030\033\256\033\234M?L\3622\241\222\000\270\313\224\"O\036\221\3561\350\316\003g8\233\015Q\367\232\007#\320\241C?\331\234\305\321fz=\363\3212\0063\242I\200\257f3\376\255A}r\252\340/\234\040|J\270\257\217\216_\300\312*'\013\371\347\032\021?%SE\230\334\377\004\233\333\377\020\244\233i0\3657\025V\376\347:L\245%\2001\237\304&%M\006\252Q\2553`\300v\272\2037\235~\347N\035R\374\034\264\033\325\032?\355\236\366;/\357\322\256\341\363\250\321\366?:\275c\340\240\336a\373\345\341\301]P8\017\347\233\377\365\201\307(\022\243`\270I\365\242M`.\252\025.A\220A\347-Za\235\025P\006\345\040\352Y\002\011\312\347\314\375(\374\241\040\011V\373\303V\203\004z\017\022\015\024\3120\0127\205\001\040\354F\251\311\303\022\222\004\037\244~\017\326\013w$\332\374\011\246m\332\240dyh\247\252\315\246Pt\253\325\367\031\224\265\367atE\227\012\020(\227A\034\205\224\003\250\344L\243\314\3758\007\351\256\3315\206*p\360\343k\350\241i\335\273\177\357\376\220{\326\001\003\277wx2\350\344\213\277\374\353\353\223\023\224\272\213\211d\232\015\252\212\302t\363\343\376\327\333\333\3021_\310\311\205\340l\265\244\251yt|\332\203:\350_\271\307\341i\207\243\030Uh\034\236\013\034&\364!\203\361\346\263\361\021\203CW\3214r\331Bj\014L\273\373\262w|\370r\301R\226Q\001\265\374\035w\040\010w\3340\203\336\205\276?J\330\364\275\212\342\367^\034\201\315\311vK3k\013\30107\327\240\007\3123zZ\333G\330\255m2\365=\230\360\350,E\377'\220\027}Lj\013\205uwx\335h2\322\320C\335\261\010V\316\374\224.)h\345\215\027\216`\234\250\213\014\275a\040\357\020,\372\300\241;\323\344r\310\310\250A01\263\271\372\264\327\322\343\247>\025\332N\351\010)\025\030\004`\345\223\336\361\001\360\363qO\331C'|\320\324\012rH\015E^\221\010|j\032>\246^\007\214\275^\273;\260\357\021\341\350t\250\207\234x\311u8$\234\326\324\227\343%ds\226^\304\2767Bg@2\363\207\3018@\037\007\360M\204\036q1f\040]\337S\027\013F\346\2605\0140.\245\006J\323v\377\307\356\201\265\017\367\262\364\342\262\202\243\177\345S\217>\3373\0311O\365\3468\2146\225}\003\364\322@O.\275\011\250F\206j\221\3557\030\372\367p6s3\030\372K\220\352\247]\234\245%\322N\356\"\013\370|7\366\345\275\352$\324j\310T\022nD,\304\022\265\273\2037\355\236\266,\211\312\367\202hgz\346\305C\334\317\217piz1\017&#ch^\034\367\216@\241>\250f\004\235\034\037\375\370\352\360\350\310=\354w\333\335\373#2]\221@\311\241^\320lW-H\324H\223\321<\246~D\364\006z0\253\256\223\000\235\240\224u\217\200\237\236\264g\263\003|\211{\014[O\267>P\277>\312I?\316\266\004\014\370\270\316IU\0365**$Ghu\007)\325\261\216A\363\212\257\002\250\212vA\026X#\266\0243\263@\354\020\262]\040\254{6O1\222\"\346\226\325\224\306\360\211\310\214t\036\207\030\352\027\215\307t\220\034Up\377\2653x\321k\037\002\207\037\276\354p;6\3674\333P\331\336>\"U\006\224\243x\277#\331\033\374\200\244\365\230D\004\361\247\304\040\011m\203\272A\2508\204\367\250co1\367+\202@=9\361`\271N|\272e\247n\033#\000\332\240\300\002\001li3\344\264\373\022X\2437\350\353\242\013\272\364\315\327\356\213\323\303\243\227\025\235\007\252\276Q\251\002\023\366\371\242%>\001\315\037d\352\341\030\012\363\006\326\310\235\306Zy\333\314{\013\026\336\017\203\366+\2359\310\367^\034D\240\373\203\361\000&\204\344tj\201\3601\342\013\032p\347\205\227\270<>\305\220:\312\033$\237\272\3300\033\307\275\014@EB\246\271\364\202\011[oa\362\316\351\026-\324@p\032\030g_\251\330\322\014\022\373n\"\266\273XKd\265\230]\341~\177\330y\207\26589t\274\243\031_\333K\220\246\272\224\201\266\250\267j\234\217O\270&D\253(+\316\035Z7\272|v\235\372u\273;\004\316\201fj\365V\3068\211\312\362\005\333\022\237\271\300\213.\305\246E\266\305\313JTz\361\343\240#*\260a-%W-\354\015jQ\347w\230\326%\030\257V\217`\266MSjh\211\257S\357\337Q\014\013\306>\371\256\221Y\347\270\017\010F\015\341V\015\363\236\237\315\317\231\306'}\006\322\315\000\257\222\255\311\344rJ\035\015\311Et\345\302\243"_buf, - "\255\341y\360\177\301\350\371\263\235\357\376\374T\003>F7\366E4\237`\230*\335\257\244-|\327Tm)x\234\265\364\335\326\323l<3>\010\222\350\233\257\277i)\257\264\335x.\370\\\273_\242\3279\352\264\373\035N\020\343)P\344/\215\014\256\335\342\344>\207o\331\356Hw\220\225\247\306gQm;\013\346a\010\346\273\207\036\351\370\325\307\316\202[\025\376*\231R\265\370\373\241\245}m-\261\304\242\026\250<\372\016lnx\027\026\022%T'\237\255\200X=\027#\246h\010\345\310U+\270\240T\271\212R\334-\213\203Z(j\250\236\203\222\225mPeZ6\232\220\312~\327<\004I\220)\314h]\322M\256M\266\331\263I\317\226\244\001J\310\014\332V\005\032\032\221]\225\272\275\260N\375\242z\271BZZ7\224\352S3#$\225\323aD\256\206\027^\354f\036;$\360\277\347\011(g\236\026\355\267e\333B,\262[\0270\345\202\"\354}1[)\226\361ZE\254\014\277\357\202\031]\271\260Y\262\304\363h\356]Y\242\226\252\350h\2131\257T\326(X\024so\350\207u\221V\224\341*xW+\236/[\216\275\252\221\327\355\000_\322\252\040\277\270\250^\256\034i\261\226\326E\030\225\324*\330.(\247\024*\307\223*\305\305HJ\247\333I\247w\370\266\323\035\000\276\275\316\313\303^\347`\260\330Wg\3338\\L\010\245VE\251\276\240F\355\202J\251\"\352\025um\001\262y\357e\011\036\371\020\356Bt\362Ek\020\\\367\364-Z\025+\2265\012\026!^\340e\\\214\264\364\2217H5&\341\205\313:\247\300,,\226\225)\352\222\351\275\257\322\0333\316\241\034\317\032\245sE\013O\227\025EZ,F\336t\"\225#_\243t\256h\021\362en\254\316\245\037\362\235\015\276\325\216\201\2534\034<;1\202\241\266T\277\342\333\245\364\030\035s\334b\3049u.F\302\325H\003g\361H\002\323\310X\224?=4\007\012\034\003;\362\313\325\255\342\215`\213\332T\241\226jy\024\267Y\260]\\\336dA\245J-\226\305U\226\266ZV\261V\313fxj\245V\315J\265Z4\3028+5h\324\251\325\236=t\263R\263\366\252\352\344\301\363R\347\2217\221\301\354\214\361\203$;\203\341c\004\267\014dG\366\227\261\236\324S\317\016u\211\231\244O;\234m\030\025\016\023\013\354\277)n\351$0\301\266\266\312g\217\021\034]\326W[\034u\301\211\003~\320Rw\273\270\336l6\361i\304\365\310\345{\031\305\016\220\235m\206\213zH\265z\\\253q\316q\241\273\345\317wll\261\326\262\020J\035\356.G\241\272\006\244\2656\350\375H\017'9i<\367\033\346[\366o\373\350\210\225\031{\223\244\260P\367\270\353``|\303(\313bLr\215\246\361uac\364\364\"\341\274V\336\026/J\277\324XrK\343%\312\327\337e\253\226\327[\263n\\-\010\353\260\035\245-:\363)\340g\224\240\015\235v\017\377~\332q\273\355\267\035<-\334y\352\320S\201M\026\324\326\240_\036>\304/U!\030\000\352\265\247\237\3765\275\203r\246\024\003e\340\026\267+\232U\267\264%\263\336K;\370\2157\"\270T\370\350\203(\031_\215Z\312\023\266A\247>\301\355\224\040L[\342\040\037\006W\201\"5\232O\247\327<\221\004\301\255x/\215\342\375}\006`z\361\265z\312\233`$\377$\362Fh\177(\315xg\360\002\014\226\367~\"\340L\242\350\375|\006\235\211f\030\263l\210q\226\377\002\343;\011\364U\002\312:\303b\361i\344\031\021\347\333\010\243I@\343j\3016\022\231v\242+Q\013~Z-\362\270O\313u\302Q\037Z\027\357\274\004\22690\215\274D/\235\244\363\361\330VG\310*\365\205\2622(\207^%)\036\013\276\226\243\215\177\304Q\270}T=\251.0h\321\027\003\311g\202\001\036\303\370\313\247\364\274n\323@\240\241\240\300g\014-\307[\304\344!\005qH\250U\034v_\035\363\203\003\273\273t$vwMq\341\272\257\016\321:C1\012\306\346\020\324\303$\335W\370\271\345(:\012\017t\327\264J\226b\244|\345\356\211,%\257\242x\340\235\2671}\211\237(\235\263\027\320\305=Mz\322\324\036\245\336y\321\204\231\210\357\312\242tK8\362&\203\032T\354u^\037\366Q\367\033\2641\001\323a\273\357\210\3261\374N\236\237\255~\012\301^~\301Y_\211\345\015\341\243W@\3072u\264=O#V\315\227UHC\353O\263\220y\032\040\220\213\372Z\344\277\322\031\2448[\215\231\326\306OR\227\277\277\266\344\254\311R\342\320\242\300\246>\313\012$\024\341K\037\017%\267\012\225H\246\370\015\240n\037\372\254\253(\207\370\3700\274\214\336\303\012p\223Sr\250?&\240\257\211\\\314\236\223m\213:\244B2\365\241\014\003\324d\3674\004\016h&\027\013VX\224\017\373\265\005\265\377\345\012\331\2644:\237\031\201\366E\371\226\230)\347~\332\236L\360qR\336\271\252P\3724\230\327\021\235\022%X\262\032\222kB\220\207\256\323A2\270\210\243\253\2767\006}L4\"\040\244\374{\263\010\364^\006\210\246`@\000e`\0047\250o\372tF\2246`'\004\250\0048\307(\035\313i%\020HV\214An(h#\013\206\003\345\2421i\355s\315L\203IW\303\330\037\353\323P\306\027\226\031\255y\303\026\332H\3748-\236\276O\360l-=\250\023]\341N\002\017r\345\223\312I\202i0\201\205!\215X\356!\350\363\005,N\254\240\\\273idkC\302cy\230.<%,\304\303\025\030J\366\3741\260\020\352C\030s=?\023k\377\230x!S\024\331\223\246\004\006\2264V\300\215\020\250\024\342\341k\240\"`\340\321,\026\231\000\310\032`\323y6?\233\004\303\314\342b\232"_buf, - "\022[}A\207\200\211\242,\307{F1:\230n\220JE\235M.\\$\037I\243.\270\004\034v\025e\006WzV\322\3770\213\265%\225\325O\\\252\271\003\274\365\365\002\015x\3522nx.\012+\352\260\304\035\012\301g6\327\325\316\342B,SI\004Q\230\265\040\221\222dr\026\030\032YA\015\277\330\273:\200\257\211b\247\250=\261\264c\255\336TzC\373R`\\rr8J\273\272\211\311H\341p\040v\0333CEU8\371\204\205G\254\300B\024d\311\255!NS\040`\021.\262\040\375\336(\300\013\350\004\203\206\307*\204\352\252&\201\021\002\375Fk\202\253\253jA\216_\2237\257\254S\2679\346\020-%\262\014\352\017\246=(gR\251\011G6[tM\330+\200\365\000`\331A\025A\262\367\365\201\363\210Z1\200\033\253\235\353`\256\355\237~&\216\302`A8\362?X\361\247\254\251\267\312$\246\303\352\354\347\211\252`\306)\377\023-\373s\031\335Q\372\216\010\036c\014\374\3042[(\366\005V})q\204$\3204\213\333\242\0268;\346\033\310hU\322\212\225\356\364\254;\026b\331\351\206\363\030\223\311\360%\215f\243\363\343-r\310\266\3322v`\347\231T((\33273\321\216\011\225b<\370BY}\024aR\277\354@\236>\354b:\262~\211ae\022'/\040\345\262\223\320\030\367\304\367\342\341\2052*J\217\2145\352'>\321X\347\036\223\211\037\236\247\027\215-\265\352\341\330xMZ\234\354\330#\237\345\277\315\240\002\035$TZ\252\000\332\"\040\224w\266t\262\260\002\352DPZb\0178\2126\226\220c\265Ww\260\337\352\353\365\246\271^K\004G^\3529\205\323R\033?\0133\007Iw\216\372\230\000^\177\346\320\311\313x;\363\225\225\314b\241\017\330f\260\2411\234\201\331\020f2\334ht\317:E\015\020\2408\026\002\000\356\342s\222C\022\332\276.\202\037?'\332\232\007\366\373E\322\314\013\370\004\245;\212\001\245\350\236\005^\221o\255\024\236(*\3145}\024%\354\365u\342&q\241\266\240\270\002\015}Ac\326\274gGY\375u\345C\2720\230\003#\347\274(C\224\353\352\313\242+\274F\006\326\034c\363\255\015q\323\266P\315\0063M\263r\354\336z\211@\257sp\312n\0028\352|\3379\332f\331\333\324\244\001\245\345wX\371\002X\365\036\253y\341r;\337F\351\247%\355\3568\365\036\327i\367YI\273O\235z\217\353\264\373uI\273\317\234z\217\353\264\373\347\222v\277v\352=6\332\265\356?/\330\205\267\357\025w~8iw_bt\320\353~\236\177E\356\014\226\"c\032\341I\003\031\007\224\224w\377\233\222\356\377\331\251\367\270\002\331;\254\271\"\\\012\036k\200\255A\040\025\240\353\370e\033\347&\034\267\323}i\213\026\301W\307\247\003\263F\347\355\311\340G\307,\374\262\363\252\323s\202Q\203\004#\243\240\005,f{\207V\237\302\202\270\335\3241).m\012&\011\245\270JA\215\035K\015\214\336\334v\320\221\323\004\276\372\000\377\322\272\370\321\240\207\275\356\216R\267\241\022\005\226X\243\025X=\034\242\266\264\335(\000\252\301\3145\230\353\227\002\324J\366\243\303>\364q\334$\037\232d\346\373\357y\037\233d\354|(\307\331a\305uP;\015\332\223\261\000\246\260\033i\0244\277\263\272\346\267\227h\376i\276yr/\275/\242\276{\372\022Q\300\324\014\250.\333h\241\274[\006-hAb\226\201\2521B\367\216\342\366]Q|\272\000E\3621\251H\323\031\316f\223@$t\237\207\324Y\307\203U\177\031\377\202\356V\232\010\231G\250\306>Z\277h\351\315\274\030\324T\260\020@\025\014BtT`\316\341\351\224^\337\225^\371\3344\344y\227ib\017\264qg\350\324H$^\304\343-\0071\330\026\022\246l\026\367d\206tm\244\020\224D\365\242\273\234\236@E\350\344Y\223\014\033bA\365\351\2162\314\020x\325h\262\017g\342\303\260h\200r\343\223[s;\316\242!U\250\334$N\003\377\313\377\335n\024N5\204\266\270Y,U\277%\253\266\262\343P\3227\354o\237\362\267\3450\270\022l\274\353\036?|hU\340\215r\300\340\326\372\360\274J\365\356q\276^QYv,\356\360\037\035k\203\362\355SC\007\0217\221\334YATZ\240\010<\254\320?Y\307}w8x\003K9\013\360\357\027\214[\326E\343E\257\363\366\370\373\216^YhiB5\305\334\013#\232\342\014\026\343\030\023\322\240)\212\223\227)\255b\177$\010\331}{(0p\276~\230a\231U\014\314\323\362\221yf\214\314\002h\317VGf\347Nt&\217\311N^\2315j\342\255Htg\277\177\322>\020\224\010\023\367a\025\374\365\312\016:\022\032\213\032\240\205\012\321\321;b\033\026!A\312'.\263\260V4\203h\027\006?\236t\270\040\244ha\226Q\364$:\347~\352^\305\336l\346\307\373e\275\001\325\263\253qR\313i4\2525i%\204\211U)%\313\314\244*\275-3:WC\211\273\220BCged\2526w$\350\276\373\252w\374\226~g\234\253\306\271\310\245\266\024D\323\340\340\012s\304\335\301\362\216\273\335(\237J\356v\243\032\274\247\034\036,\366;\213a6+!\267S\261\355gJ\333\360\373\364\316\355\363\3160`\325p\370\332\300\001~\237\335\031\217g\012\036\024`5\\\376l\301\005~\277\2763>_\033\370P\240\325p\372\246\000'\370\375\363\235\361\372\263\005/\012\270\032n\337\226\340\006\277\337\334\031\277o\012\360\243\300\253\341\370\335\002\034\341\367\333;\343\371m\011\236\264\201j\270\376\245\002\256\360\373\335\235\361\375n\001\276\264\221\212\362p\273\"\322\360\373\227;#\376\227\012\210\323\206*\"\277S\003y,\263}w\021\275]\261\013\254\271\302U\011\026\256.]\271\360*\305:=h\222.7?\273\205\300\351\012\011Z\303?\355!\321P\273%\202\034\007\360\354(\300\235\303\333\275\342\342\203\244\365O\333\246\234\242\3028\372\316\225\200\273?Hhsrc\362\006\017\003\231-Y1\244\341Z\032\256\274\230\200mA\271\024\320A\375^\030-\356\037T\351\215\322\366?\325\200Rl!\034\355U-\036\373\210T\215.6I!\254a\354C\235\245`\031\200\242\360\322\217S\016\351\237\2051\367\304\322\363\375\001R\216\005\316\361\320\272\001\236\0143\240\220\022\264\310\321\216\326\313\016~-+\376T/\256\177\355\371Ij\033\242\375\243\235\375\316\016\202i\002\204\375\316S\366\021\213\363\341\327:!\273m\324n\232\025ww\261\350*\272\\\257\017\222}\241\021ww7\364J:#\253eh\326A\366\040\342Wj.\030\227\000\367\023\224\027>\236\223\267\361\377~~\016\212&pt\360\001\255\213\300\363\354\225\253\243\024\256;\016\265\272&dN\007\233\323\307\217]\255\266lg;\002\377&\021\220\252\360\244\205\022\022R+\303\257\022\036j\273\214\245s\234]\040h\210\215`\257\202\320\2334\325A\260\012o\201\200NMD0\261\311\272}\016\266\270'\21119((+\033\231\304\244\240\367k\221-\341-)D+\225\010u(F*\315\260Ab\225\344\202N\312b]\271\377\312\"\337\322{S\250\232t\007\203\023w\307\241I\022\3616Z\246"_buf, - "\313\030\235/\325\321d\325\206T\014\272i:\313+\003U\241\324\324\011\260\261}\305\363`j\003E\274_\275W9\305\007\233\254\241\374\324n\250\276bd\303\310\256\034\221\005kH=d\255B\270&\01063\352q\207.\245\355\2757%\265\312#UV&\255|\351\342\364\211\323\254\372rW\237\220w^\3654*\347%\370B\214*,~\313-{\365g\355\222+by\027\357aU\\\320\340\242\225q\255xW\362\340\010\310\203\331g\335A\007#P\360\320U\227\356\230T\254\262#\2534IF\343;-$\374x\017=\264(\200;\225\021r\177\260at\247U\262\024\255\222\035_\005\204\375\330t\245\372\2178\340\314\250\372j\036\016Kh\224\031\325(k\330K\332Z\354\237\323\363\201\2166W[M\021\027\214\230\264\303\321\300;Oh\3202\377\314\341\337dW\332\361\362\374\250\262#\276c\212\001\3658\355\206@\226\265\323(<\267\334\314G&Ca\025\005.(o\253\221\311F\245\025\314\010\235\2109\265\356#\320Qm\356\343\221\323}\333\031\2749~\271]Y\320,\315\201J\274?\256Y\254\255\373\244*\002e8\300\362\001\337\313\210\252\240tgZ\336\223\314\256\312\241\277\015\235UT\356\217\334\271\0052\317\275\007\002t\243&\220\035\033\220J\242\030\252Q6\323\335\234\002\032\331-W\2253\204\3673H7\312%\243t\340\351\240/M\040m\275\263u\356n\226\367]\373m\030\320\313v]_\342M\326h\324\253XIe)\344\001\212xy\304\217\304kw7\223V\002\014\233BNM\234\357A_\274[\177\362R\301Yit\021\025\277\333\245o1^\306\026\367b\354\271\251\301\231v/\225\032_\362\271\224!\277e\241\245\001m\327d{\205\351\027\2174Y\247\031\016\327\215\200\334\212\363\253Y\265\034\371=\025\374m\210\263S\261\334vCa\200*\341\246\005\213\244}\211\\\035G\025\254\314\315\312\005\311\357\256\344oF\243\235\252\0055\346R\330\242\012\237\011\343@`\207K\321\352E\225\315\004\371Rh\205\205\266\253\225\252+\205$w\010\266`N\215{a\213/o5O\333\202\327\015e8\252\255'\332\262\364\021\324\221\037\376`%\356X`\241&qg\025\342^\306\334\334\014h..A>\351\"\013G\355\036z\274x\245\257=\370\371\003\031w30mq\254\315\012\201\247\315\305\341\265\315\305\241\303\315\305\321\320\315\305\201\335\315\305\261\351\315\305a\365\315\305'\001\232\213\017-4\027\237\253h\230Y\013J\016\3650\233Tq\212,\345o(;gS\327\027\361\331>\256Q\030f%1N^\255\320'P}4\276\370\013\3760\376\202\373s\030\254\236\335\2768\023~\247\316\204\025z\023V\315u_\374\003\237\266\247a\025\256\206{\345\231/oW\341\206X\221\037\342\276U\240/>\212\217\344\243X\225\223\342\336\225\224/\016\214\217\354\300\270\243\007c5&\353\027\357\306o\354\335h\330/\241\314ex52\300N\375\324\023\231_\345\325\020\030\360\342\246\261\027\244\211\365z\210\322[\227\304\221\304\311\225w\235\270\364\362Q\262\313\262\336\322/<\227\270\310J\234\017\230\227g`\360\"T\367<\300[\2579\000\372D\326WZ\013\022w\350M&x'\036\215\234\323.\225\3117A`\275\325\"\350I;>OZY\015\331\362\276\314\335\302\222\215\303\267Ko\262\017\365[NC\177FA8\015\234c\364\234\022\015\022\246\3210A\230*Y\342\355G\004\364\213\257\024J\251\200\020\266~\205\213\245o\3728(\224)\256RB\216<\030\354\274\203\005\020\233\026\014\215$Q~\024X8\320>\205\317\253\264\234\355F\203\215\277~k\361p6s'\301\231\013PX\2269\000Coq\265\277\"\255\347\344\351\366\316\267\333\317\304\205c\224r,\277\235\033\321<\362#`~\177\210\031\300\361\242\263\203\307\217w\276\245y\263b\177\032]\312\207O\267\267\310\033?\034\202\\\305\3139\345\005f0u&\300\366#r\025\244\027\014:\273\222\310e\215\220\013?\366\267Jh:\324\211z\332R\356e{\305S\373\261\244\351\003\345\332\021\206\034\2641\006\360\200\225\313o\007\343\317\207\227\342\201\206\214K\251\014-\236\322\223\033\255=\356\356\344}\371\233\357\317\010;`A\321\226\327\257\310\333\341\223\371l\026\201\\@*\355\254\262S\262\262\265w\373\326\327\303\313\334s>\254\264\227\316)\345=\3550\2168>\"\345\240=uw\366\000OF\337\250<\036z\371\344\331R>Z.\311\311\213\300\203\326Zv\345\024\017\237m'o\375\364\"\032\301Da\371\342mw_\321\310;\347`w\367\321\224\316\034Q\251\201wK\251\327\325X\000;Jm\243\256\232[|\227\350\240Y\332X\211]C\336?\242\334\275%\223\314\343\305\204q0R\357\2008\040\321\331\2773\251\346\300\267-\033\366<\217:Lwz\300\320\014.\246\3159\034q\301?\006\352\233-\215f\230\370\337J\373\342\006\026R\307lBO\273\036\372WL\326c\246\2758\272j\330\306a\377\240e\322\224\362\2238\321\251De3\340\312\023\313\315*\330)\027\246\217v\004\040W*\205\312F)\355\372\035\355\222\033\012\323|\206\020\366\350\370pDy880L\376\316^\031*\256\023\213qK\274\350f@\013\376t\272\034\307\214`M\215H\012\035\304#\263g\377\023\3704X\017J\256\033\\p1;\273\206\247(\271\012\250\337\007\355~\007/\035g\206{\257\215A\254\016\321l-yKa\311!\241*\255p\327_Ic\272#ZkW\271\302\260\340\347\237\332\225\027K\307sk!\334\266P\356\262Vo\367H\311OV\366\226,\3721\302\310E?d\034r)\301\301\252i\017:\314F\263\015\260\373T\245z\346\250\341\337\201%\201\245\265\010l\274\3747C\251\202\353\206\001,\245\271l\241Q\267;vNR{u\3002\277XY\253\274\227\244\026\323-\377\2433N6\261+\306\251\223\233e\232\271\313\326P\325\261\334\253\303\346\253\240\337\307i\245\356\306me\326g\274_\377T\201@li)\340\250\023\201\272PJ\231\245\232X)\273A\326\205\237\003\267\355Bm\367\015~\031\2700\227\335\023\367\210>\354\210G}\370\327\305\304\341\315U\202{\345\236\272](\312!\253\235\317\235\2041\023\230S:K\273\343cR]sc}\031\0229$rP\230Q\364\321\346\020N\370\262\021\325\027\224?\356\224R\350\260\272\251\264R\342\377QgV\321\310\374V3\252\310,\260kl\246]PmV\025+\214+\244\371\001}\336\207\377V?\236\004/\025)\320a3Q\271\312\271\266\362a\251;\337\376\230c\366\033\317B\233\204\255l;}\231\225\367\275\000\336\373(}\231\244\265WJu\252>Y\355O\025\267\332\323\372\336\272=\245\204\221\345\242?h\367\006\356\273v\257{\330\205\316\366OO@\256\364\361\302\274\222:\274\220\373\372\350\370E\373\250/\253\333=)zv\216\322\261\364X\231\040Ic/&\215\302L\036D\246\362\040u\223\317\344s\213\334h\203}K\032\230\372\357\311#\322=\006H\003\362\350I\031\365\216O\026\021oy\007\252\223\033\336b\226\250=C\0245\324\270\273\353#\3635\227(\355~&e\034\362\367\2717\011\306\201"_buf, - "?\022\016u\223\022\277\02366\372Y\312\315\353\033\353\344a\2160\037\207\237\177\023i\247\2568O\313\266\015~K\266\370h\273\020\271\215\007\375\301\362\354I*\210Y\266\015Q\312\236\017\017\312\0228Y\230\262\200%o\357&n\227\330=\3219\315)\333\225Z\314\245w\021\304\366\040\373\337@&\313\030\356l)\022[\332\277\231(\276\017\371+:u?Z\304\347'ss\352\376S\247\356\266\341\307\347\213\202*\377\350\364\216\321\2569\004\273\371\240N\305\323\356i\277\3632\243\206\245j\225\320\345z\033\242{\226\305EMqW{\277\360\246\3348\327\256Y)($O\210V\355\304bh\271c\035\313\201\266^\364B\223\007\227\257\3077\37156S\311-/\361'\010S\274\221\322\377@\236\223\355\275\372[\260\342'KP>\274\000\251D\277>\")Foa/\222\237~\206\006n\312/\221+\272FR\017\244\276\335\263c\300b\333\330u\232~\014\255A\327~\372\271\240\260\203\013iC\024\276q\264\204\231\311\315\255]\006R\245g\235l\302\357c\026\360\226P\261\351(\375\244\304\374\271\301\004\010J\312&\243\357\343\307\030s\012\232M\201\334\224\012\202\251\011\231CN\355-\034\266\2625\342|\022\235y\023U\015zN~\3729\307\00529\\\365\033\001\215\373\016\015\364x$\326\266\272k\256\025\272\265~\\B\023\252\032\264Q}\343zI\307d\345\345\246l\253\264\312Z\365;\335\214\316\234\207\367A\311\272\036\300\337\361\006\263\274\005\364\236\371\034'a\305}\314\337\357x\024\372\313\227`\367\345\011\372G\341\376\022\327\266\266\365\264\200\342\300\360/O\017\006\031Al\006B\251c}\000\212\000U!\370\325\011\230{{9kr\201\356U\331\224X\022N\201}Q\033N\241\271Q\000\247,\235\257\351k\026\243\221E\261\256\332\314\250\030\226X\311\030\271kL\345]\315\227\002\2606c\203pk\243\016z\246o\360f\311n\346\322\253'N\243\0060r\277fN\201\221\361\211\231K\245,!ET\243\320\224*\300\016+\271\023\220h\367\211\235\224\232\025\260\243'I\302\371\224\321\014Of\005\377\365\243\261\223a\332\040O\362\017\177\332\376\271Q\321.\254\230{\237l\250\362h\237O\237U\270\335*\233\234\320QI\211\237\033Px}?W%\243\000\253\362'\263Jk\275\232\335j1[W\0205~\273G\226\374Y\255\215l\231\263l\336\263!\017R\365\270\034\277\302)\323\023\3449K\345\222\266\375\2123\022\014\352\206r\305Su\253\274\352\234j\311\353\233\354\276\"\332\273t\217\334E\336\246[\212\350\336[\036\216\364%\334\205\245j`p?\001\371\253\207S\331Y\262\010P\231\256U\007\241b\275\354\336\214\315\234\246\236s\011,62-\332\376gn\3727-\211\024\357\231\232\325,L\013\251?w7\313G\367\257\344h\270\310-\360\307\231\001v\363\377N3`i\342\376\301&\304\212\256e\310\357qWD\031'\324\027#\254\376(\336~f\006\220\323Xf\345s\254k\\\205\035\263\334\002\372y\252\023\242\313\244\"\365rG7V\2214\341\017\021\344\265\"=\202|j\201X\367\235\004\242,\366\313\232\015\372\343\306\200\321\316\225\004\202\321\367_\242\301\352F\203-\012\377\376\335E\206\345xfe!b\237R\230\330]\362\234\334{\030\215%`\277\344\210r\235\025\362w}J|\231\000\233;\221z\311C\246\277\377\223\337\037-\340,;G\274\314\271\341?\330\210\25542m\245\224\377CO\244\373\012a\313\250t\007\273\310\032\326\366\2070\223\026_/\177\327\303\221$\273i~\3619\311b\025p%\366\034E\252An\226\014h\273O3\257\330\040Z\312\0362\035\311\225\014\242\025\230\032_\002\277~\207\201_J\320\327\012\216\022\177\232!_\237\202AZ9\012\213/q\237G(V\351&\300\"\317~%\333Z:\341\357\301\300\256\266@j\347\326\263\005O\236`\377xA\023\345\246\236y\312w\0115\353\363q\274\337\243i\275b\272/\031T\361\373\034\224\337<\356h\011\243\357\217=\243V`r\337\357(|\231_\031\251V\030\306\264\374\236\244m\353\367\363\216o*\216\005Z\211\241l7\222o\367>\211\015\317\337\211\355\3729\330H\037-H\351\023\267\207\214\330#!w\214\000\244\337\306\364\3704\014\216eD\271SC\\\327^#>\367%T\215\335\321n\347B\222\2731c\350\353\255\013\363r\303\2417\303\205\227\335oh\274\203\206\200\215\203(\274\000\2313\361c\250]X$\010\307\221\345\275\270x\015\206\177\353\302vO\242\270|\255G\013\276\363\222\335\335\3431\275\025\315\017\347S\365\3521Y\002\226\037|Gx\271\354\272\255\323\360}\030]\2050\0257w\232\362\351\361{\024\301\331wz\353\322s\242\224x\347\305!\316\345\347\344isM>}\345\005\023\240\314\013:\267\267?\354l+\357:\037f\320\263\004\372\215\245\374\021\224P\212\377\252\002\207\242\223`\030\244\374\275Y\362\251\006\025\257m\302\234a\254\301mx\237\025V\012\016.b\377J-\235}\326\332~\031\214\302t\200\267q\025\025\326;\234z\223N\034G\361A\004Z\227D\344\251\201\310\032\277]Q^\260x\026E\023\022$\307\357\035\31302\016\240\037\371Mg\274\370_\347(\354\306\221C\205\362xB/\257\3323\030\342e\220\314\242\204\342\262\273\373\212\226\261\363\205RP\360\007+\236\261G7\212\247\336\204vi{G\3517\3646\015\302\271\177\034fc\004E\2366\031\"\374aB\306\360\201j9Mr6Oa\335\366\207s\332\336\220\003H\024R\202i\201r\217\201\372\272\251\010d\000y\022\203\370\373\200+?\347\"v}\342\003Y\277?\237\321W\3306\003\361\235Z_\242\344\305H`\274\223\020x\020\221\032E$\214R\216\352\205O\3215\207\253\220\262\321\314\217\2754\212a\264\235\342R\223\013\0207\205o\343\213l\024\351@'\027\321|2\312\321\330\030u\2462\341%h\202=$\011\265\2220\266|\235s\370\243\015+.\222\376\015\362\200FS\335\232\030\251$\316\343SrM\232&]u\371V\040\336\304\245qBV\"\327\323\027\031of\227\276M\275a\034u\345%t\364\245~c\234\270*n\317R\233\213\363Q&\240\262R\305cf\276\330\313&\007\364\366\235O\256<\040N\032\221\221?\361S\340\252\213\200\337=\207\035\003\206A\326\363\340\311t\006\322\020\277\236\343\275\241_o}G\246\276\027&*4\344Jya*~\013IJ\375\307#\002\242=\214\302M\357\374\034\326+x\242T\323h\347\240V\306P\311nz\2558`\346\202e\254W#\037;\021%\326\305.\215\230\252\272u\241\336\004|\351#\011Z\312\223!h\266\240\340\264\212\256\013\316\0363x-\263\241\004)2\325\233\011\242d|5Z\320L\304j\266\326\212Y\021tn^j\003\006l\236\212\373'\2157~\034\027\274\231D\364.A\372\212\335\344)\331oO\343\367\303>\255\242p\371e\020"_buf, - "\247s\020\301\377\343\257\034\345\312_\361No-\341\345\370\035\2338\225\265\273}\345u\226\034\240y{\"\331\030\003GRE\265A/\260\3448\261\3155\255\027=\177\236\3405\216\014\002/g\273\340Q\322\004\267\270\334\224L]j\250\354\351/y\027\036\301\353(I\330K\365\226R6#\363M\2524\371_Q\001Y\202\366\037Z\222\024\332l\251\026\231R\262\3602j\011F\212\377\375}\220\256\003q\271\344\2457\231s\352\331\320\331P\250\202?\217h\177\021\006\255\270g\313P\367\010\005H\366\346V\307\343\334\007\246\224\035\221\254\040%?kA\210\364\333\374\205\264r\372\030\323\012$\274\037\217aN$.*\010.E0Q\225c\313\264.\232I\331\343\227~\212\313\355\215\351c\351@\033T`\353\004RD\275\033jr^\362\016k\233\335`<\363\202x\0370W.\006m\265\210@^\031`\3120\242M\215I\364V'Q\364~>c\353\235\030[:\324\312\200\360I\246\337\016\314\372\251O\361\267\363\024\031\002\233\375\236\"\304M\347k\353\254/,\255\241+\312\263\366vw%\0359C\262\361\362c|\256Nx\034Sf\022e\317\274\311\004\213\201\306\242\022\026\272\336\322\330;\241\206\376v\351t\351d\323\345\0360\013\300\354\017\274\011H\224\230\356\030\357wZ\031j7\026W\006_\311\034\276\317\216\367\247\343u\337\374k\007,\305\365\003/De\020\2121\310L-\206\031\006\205\327\033\305|G\311\003\377|\317Y\314\360k\261\307[\2405\370\361%(N\014\315-l\031o\326\325\313\217\243\230%M\246\215\323\252\040TE\317l\207\0029\370\331<\271p\317\274!\030\025\274\303C\017\310\202\2709\012\254\\\203\\H\350C\221\015@Fv\331\224\012\341V\327'l\032D%)2\316\355\005\035\234\234\354|\313]b\356\367\207\235w95\300\275\014\374\253\226\272\243\204@\\\367\370\305_\301\3727\205Yt\366o\340\201x\310,\367\207\301\224\336L\276\377*\232\207#\017\025\234'\331\307\255\213\226\002\357\302K\334\261O\375\355\322\021\303Pm\367\016\334N\027\357\372}\251\227sDkb\327\244\270\336\266\354\000\365\002A\235\2767\366{\240\255y\0110K\267\177|\366o`\263Gx56R\036\312\300\272\003l2=\246F\2517\351CY\344C\020Q#,\005\263\246s\004\\<\241\2129\336R\374\040\327\354\0327\036\252\264\311\247\323O\370%fe~\306E\204\203\250\205\020\207\025\214\035\001\017\224\350p\224\014\"Qi\027\013\376\254\260:gPZ\2347\244\227\335\323\256\325\016&\210\233N\357\323n\277\375\252\003\177z\235A\373\260\013\335\317\217\007p\332q\3675\037\256j\324i\334,M\004\311\\\240K\001'\273k\017g\261w>\365\010\375NF\201w\036\302J\036\014\011\316\353\342\267\001|\000\363\205\254o\276\003<7\015\372l\002\312\357\223u\301a\367@\371\032\375\210f*\036\265\206\013\340\317\303\004\006\001\376\304\270\226\204\376\250p\010\2410\310\207(<\227\023K\023G\232\0340e\307\333\376\201\373}\247'{q\305<]\016\016B#\367t\024P\355n\367\353\235\357\266\033\334\362\363R\\\011\251\361\307\324*\260\363\306\"\265\275\343\215Fhf6\310\3315_\003\233\344\352\"\000-\011\267\273\251A8\201\336%\304;\003S\203\232\211`\364]x\361t\002\325$\272y\025\253@\303\222C\351\177\000!\034r\235WQx\311<\234\301\3374\323R\2255]-\027{Wo\375i\024_\003\277\260\375\007\016\214\316\221G\021\235\022MM\317\307?\244QU\245\256\324\330`\203\260\226\214E^,b\371z\033\0223\266\320\263\257\015}\015\253\206!3{\016\023\246\307\037\206\250N\030f\216\035\006\253\240\204E\234\266l{-t\325\247\036\177TNrK=(\3702D\214\022\013\277\301b\272\317\355\212\026(\023`Eh\257N[xp\236m\034\305s\237\372]\234\206\251\353\3460\316P]\200(&\027\331lQ\370ct!\321\006\024\350\246\001\247\000a\003J\035LL\253~\236u\017a\357\253\326LS\216\177\313\331n4@\363\322m\245\333\252\232\250\312e|\333\211{\301Q\313\311\330\246Cj\260\256|F\241\303G`\0127\030\357k\335~\300\324V\246\004\355\017Z\274\017dc#{u\006\013\214\033\215Y\327}\341vn\022Y\272i\353\010\337\037\313\372\303h\206X(f\251\251\025\363)#Ts\213\034\310\233\233\367J\205%\210P\225\012\222\010\376\207\0022\370\037\266\256.\274\324YB,T\352\267\275\333\262H\263\376x\012S\324\332\2352\326\346\025\265\216\242\222\310\226\323\221\343\276mw\333\257;/\033\212;\361\001:\375\021\"\254f\007G=\302g\020\210\001\217\314\323\361w*\372\365\305\375p\022\203\205\347\307~8\3643<\007\377\202\336\214s=\004L\035|\376\3749\011\347\223\311,\215\033E[\273\352V\373:\0266\2158*\310\316\256S\032\225\335\277\006\373g\272\273;\200\265\022M\325a4\202\212\273\273\247\203W\337m\266^\373\351\013,\210mo\266$\226\015\003\342p\022\354\356\316\202\320\005\274\366\005D\254\330\"3hc\2036\366\323\366\317V\033L\3057\366\251\3354\203W\314\220\313b\334\311\243\2263\003\261Nam\266\216\374\360<\275\320\306S\215\002*qJ\300\213\3031\271\362Q\360\006`W\201\202G\016\036?\336\371\272\211\021\365\3419:\256\301h%\324\030\345l\355\246\314;-\006\227\354\253\021\230\362\363s\252\027\264T\337\007\243\330[\357\275\037+Cj\201\363\012\212\000\000\215Yp\311X8\343vwy\364\205\020j\371\265z\037\201KAb\231ukF\014?2\275\303\310\216U\371\2743\231\262\3109I\342\304\360\007p\272w_\354\202>:\231\360\250\010t\362\013-qs\022@\377\223k\020\306\037\350\014cV\310\364,8\237\007\3515\335\372\262\201\234C\0377\371$\226d\035I/%\250\006s\0326C5\363\255\374\234I\222\255\254\254\303\272\271W4\265\2604u\243\226\312\313U\014\352\203{\033U\222\033Va\232\221\007B\030j.\211W\355\243\243\027\355\203\277\211@\235W\207\235^\243lQ\265IpAX\375\032Z\245\362\302&\025\020\322\234\322}1\213\254\001\340\226\001nII\303\004,\231\031\022\031\267H'\023.\336\203q0\244\356\020\300\354?s\214\243!`\232\240\240\210\306\314\360\330R!\366\3516!9\363\011\010-\220\346\320\333k\312\343\300\204c\020\276\327\344?\342\302\304&\241<\236\033Y\321\356u\031'\025,\"\262\256#M\005\277`u\224\355*\"i_\347\277\030\314\210K\337\035^\332\237\307b\271\242ky\312\203u\330\037\301^~U]bY\365tA\347rDuT\277\240\336-*\031&\327\350T\303\027\373\035\321\035\307o\324\321\024\226\031.\\\350\027\367F\035\252\301\277\012\311\\}\345\353GS\312\251Bb&3X\004\321\355KY>Y\323V\271\375\242\225l_\2255\332\226\0065r,\343\351\344lp\272k\327\310\366\351\252yCk\243\307\334\245\325p4\253\351\030\252D^\214\202\246\270T$\221REo\272F\223\265\032{Ta\014\336\035\274i\367\352\320\375jY\276\270\022\214qe`EV\315\032WK"_buf, - "\362\306U1s\220Z\334q\205\344w\323\232\014b\324Z\206G\004\210\272M>*\236\013\270\254\276x\271+VBP\254A'#\277@\371p\342\207\2770=:\301\200\251\024lM\252xG\341W)9\217\010z\373`UE=\360l>\206\305\245)\040\322\237\253\213`\342\223\367\276?\313d'I\374\251\027\002\272\311\377\351]\306\215\312\376?J'\307O\375\177\374\\{.\262\236/\261\352H\2407\010\201\334\332\366\217\352\364\040\011\316Qb\327\354\210Rku\375Y`\246\321\006\356\334\337y\270\\\217\265z\237@\2375\345\301\"\303^\3748\3504\352H\2573jXW\227ZX\236\250\226\215\234\3008\323V\204\031\335\015\256\204\223\214(\250+\273&Q\345U\005\213.\337\010\251\333\322\362\315If\255N?\265\312\035\033\255\321O\255\316\012\232]\276m\033\002\0251@\277{\3056\251\213\376\254v\013(\024j-1u\211\250\310\266eV\200\245\307\254F\203\206\000\256MC\012\223{9\335\264\216\244\223\225j\2679\236D^\325\246hYb\272\212\224\243C\260F\014\203,\252\266:\026\243h~6\251*\333Y\341;\340\2217W-(\0154M\321R\365\264U\005\331\323Gdf\363i\317l>\305\242\2259\277\32193|u\267\004\375K\305\040\3279\207\254\033\325\012\274I\226\356\366\024w\357A!\325z\344`w\267\252\216\315\012\177n\364)v\211\324\343\257\177U\244R\321\316H\0211\254\333+\000\300\246\242\251\346LI\304f\336\211u\030\316\346\351a\312\374\307\012g\364}<\310\340\343\251\014\255H\321\306\177x\236\341\250U\040\343\040\306\243$\022\340\304\303#l:\011\252\371\340c\026o\273~C\214\321E>\243\315\340!\010\033|\245v\211\266\374\210\302\2608\317q\203\305y\374\230\276\336#ZK{\204?oXO\263\012\224\233d}\231\306o\255\375'\267\353\366X@\233s?\363\377iQv\325\304y\267\317\276U\025\004\242\354\251\025\026\024\037\234\236\300\277\325\313\323\364\205\335A\215\032\007oz\307\335\343\032\025\216Ox\036\030\255J\026\320\247\216\020\013\3727<\370\013ij\322\250\241\004\030\317\323`\022\244\327\255\202H;Kd\303\216\232\365\346i\271FO\217(`\015(H\352{\207\325\352\033\004\277\335y\031\314\211\317\3625\006\333\334*X\255\370*\265\014\304\304\207^\216\354\040\353\257U4\256T\312\212J\263d!\317\330\270\262\201\301K%\276#QG\345\257\210\007\011\327`\260r\226\022\000\367\007\313p\224R{\203\210/\3131\025.\313\002\302\026F\304S\253\314i,\251IId\252j\357\222\247o\327KU\240\273s\217UB\345ES:\237\201mZS6\345\004\262\312<\024b\253z\020pNi\327\203?%\207!\330f>j\215G\363v\365\254\000\362t\360s\342t\311\276\322W\027+\354Sh\"$C\237\322-k\032\031,\337\231\370SP\357Ob\352`\266\036\343a\361\3064VQl\364c\315\015B\333n\032g\340\242\304\306w\021\345\021@\373\377\230B\275\013\277\353v\275\273\234=ic\347~\272\337m9\264\375\206E\351\267\364\214Q\247\0114}Lv\200H\254;\274\007\200s\241\315i\213H]n4\327\352\216\206\304\271Ih\250o\253\326\360\030\003\003\243\222\357\325\355Z\201\354\223\011\210JE\040%\037K\332\003\025Z\365\245\240\001\200\263\324\235\326\326\257n\276\322\237\013\036\262\021\330\332\013\2039P`\321\363\236\215\275\025\231t\213\005]^;\\(\277,\012\342\242e\222WQ\005\335\245\027\007^\230.\\$\313\371b\032\341\011\024\257\362N\221\302\021\262\352F\201uc\2543v\237\231v\025!\037\363\005\274\314{\276/J/\307\3206(\030\351F\237\332\354i\376j\213\012m\346}\313\275k,\364\023.\232f6\243\252\200\3631p\227\235_<\363\317\203\220\035o\032\307\321\024iH.\374\230\005\026\262\330R\266\363\211\345\366\314\247P-\027-8*R\033\210\312\307-\363\3606\312}\032;f\014\010\217o\305\027,&\270\374\320F\205pb\245\321\040q\251\307\320\015\2406,\343\306A\024\005\225Em\225\203\336Wq\221=\335\227\347U(u\365C9\003L\311\334\020at\0027y\010GG\315\342\354\030)\321j\013\335\310\002Yhf\304\231\334\300_\010\217B\1775\266\376\326\013\275s\014\036\246\316\016\314\225\023\372\040S\230W6\251\207\212\335\235m9\364C\207\253<\264,\333r@\310\255\265r\2571+%\217\230\323/\252\230(\3363\320\240\2601\025\365\2338\305\34471\333oe\334\317\033\232\361\213\360\003\341lw\225\353\374\223\353\202\336\264'\223h\230\371\302Kz\244\0366G\330M\245\256<\017\242ur\261\226bu\202\323\221a~\351\347\004\2315{E\017\246\263\275`<\220n9\227\341\360\232yMZ@T\306Z\036L4\243\240\015o\267U\315)\021\351\200]\303vt\311\252\040\331\225\243B=\264W\274\313\325,\214_\227\023\242\247\035\371\252\022\325.kH1Rm\177\250W\314\374\332aI\215\307x\331\275\352\033\200\003\232\023\240,piP#X\211\037lr6\2748n`\275Jhc\341\302l\004\266\314F\272q>\274\210A\305\003\251=\217Y\244{M;=\357\326T\365\327a\032\340\011\312\354\001mDKID\333\267'M\311(\316\363\355\260\312\234\314\024\024O\005\312\011e!nr==\213&\310\330H\224B\210\252\340\311\300\356\263\367\360\216C\341\355p\306-\0271\302\000\372\351+\374C!aT\302\224>|\362\325\232b`\363\227#?\244/\177\376J;\320\257L\316[\245\007VB0\345\331K\323\250U\221&\225\000\216\375\351\212!\316\202\341j\001\206^\270Z\200\323`\030\257\032\342d\022T\205\310\327UeW\206/\264U\246mR\040\277\277g\307\203\024u\202\362\177\231!f\266\264\317a\260\252u\"r\312!\011\211-\336\336\311\353\040\200l\015\243y\230\262c\341_\021:\021\313\3477\226K\276\252\355@(\241\366r\324eGm\360\321\376Nk%T\326!\336;\265\327I\262\376I\221\361\233\355\225\323\021A~\004BN?-B>\373f{\365\244d@?\0021/\352\023\263\232$FM\307\235E\250\017\032*\024/\375\332\017\375\030\310\244\024\034\262t[g\312\356\010\036\015\214\302"_buf, - "\311u\021\360\375\204\036\251v\207`\375\274o\025\014\364\001\276T\304\374KN\212jc\2574\306\001\311\372\313\214z\01181\336Y\221\372\001\037Y\335-\3721\001\325\322w\375Y4\274px\362x\372\210\320G\353\005\223\245x4u\202\333\3066O\377\245\351\255=W\033^\335\030Tib\361\270\320\274\001\274)\232\314\272\020,M\330O\001\245\312X5\366\362)\207r;5\351\224b\300\323o\337\334\3523\367|\312\306\333\331\020\205\232dC\242d=_\314\241>R\301\262\035,\012\3131\252g\216\030\263\333`\313c\205~\352Mg}L\362#\323\350\255?\335\336\371vs{gs\347\233\301\316\267\273\333\337\354~\375\347\177\230Y\027h0\264\004\360\223\006\312H\214\300\032\343'\364\330\227\361\024=\011\353\177\372q\363O\323\315?\215\006\177z\263\373\247\267\273\177\352\377c\275\012Q\201M\306\264\263\262\325\246\336\227&6\000\244\0244*\246dUP\032$\355\300vA4\232\004\262\314f\215%\320\246\350\366\200^\347\365a\037\236@\325\323\267j\316?q\363m\316(\345\027\006d\033/\266\331-\340\264\264kO\312\374\025\242Fv\010\237\374\2638e\020\363\376\373\"\225\344\363L<\236\373)\317\230)\362d\276\231\2379\215\255\354\271-\223\346\226\236\372\360aF\207\207\312\355:M@J\275l\347\226h\327DhC\231\235\262VO\007;\002\345-\221Q4\237\247Q\020\240\241\337A\301?\337\342\303\3335#\363\331\202a\254:\350\372]O\2133\242E\263\206=\301\232\222\3659\237\215\371\256\231\326\236}\367\027\352\252\376\352\371\363\257\320\251N\217W<\221\347,\246A2\245\256\236B\000\333;\337Q\000\323(\366\311zQ\375\365\022\014v\236R\000d\232\035A\271\003\206\327\002\2700\036\230\250\014\252AO\345\225\024\040\234\326\227\310\347\247\323\355\327_\277\320m\031\272=\177\376\205n\313\320\355\301\027\272-E\267\326\027\262-uu\311\027\262-\305m_f\351r\354\366\205n\345!\302T!U-\206Si0,c/\334\253\015\300\216Q,\324\350\027\252\351>\277\3611\353\253#\365\362J\3727\035\374\246\346N\243\021x\016\000h\224\251\341\252R\255ow\221>\337\351H4\266\221\276\227\004\035'\354\232O\342S/Mz\255\261\327\231\237^\371>\262G\012\274\026\216\010u\374\203%M\234\356\351\321\021\360\307h>\304(K|Kd\302\367\032\026%\367\306c\223~\007Q\340TS\255Ye213\"\3571\261R\015\3257(\337\020n\224\302)nAb\360HC\001\235c\005M\213\206\314|R\224YY\272\252\0267\177V\200\010\315\226\363\333`\2024\2408d(\025\040\262\000\003\312\265|p\226A\204\245\015\272\017L\356\302\275\335(-c\340\215\232\274\373`\011\336\315P\250\307\276\017\356\201i\012qY\304\301\367\213\314\212\231\370A}&\316pY5\037?\250\302\307\352\262\214\313\017<+q\323\025\256r\274\252\272\306\301\332\246.K\013|<\331$*0\273\255R\277\276\242\305\011xc\310\020\212i\223\001n\212o\353\317\237\257\263g\267\205\367\024\352H2\237x!v,\204\276\024-\246_0\021\264\010\221%\311\371\340\036\311\231q\263\235\242\017jP\364\301*)\372\300\240\350\203UR\264u\017\004\315/\003\254#-\272\016d\035i\225\021\264^7\366?b7\366\215n\354\257\256\033\255\347\037s8\236\233\343\361|\205\003\3621{\262o\366d\277ROH\265\256\374J\034\243#\367\325\217_\215n\374\272\302^l|\264^l\030\275\330Xa/\376\365\321z\361/\243\027\377Z\241\320\245\233?j?\032+w\275\320\346\355\336\027\201\006`\021$\237\234\257\252t&\376\372)\320\015\260\370\315\351&\357\015\317|4\212\023\357T#H\313\316\372F!\241m\334\026\371\306\250\236\316\\,\252\227\312\352\375\222\327{\315\342\222\315y>\316\005\200\271^\216ge5pJo\362\225\260\310\226I\027\375\034!\217\230y)\210\034\227\346Q\\\344:\315\354A\306\214\022k\311\200V\342\347\212\335\220I\241p1\033v\324\023\273Z\253f\032aK\213\264\310\015\257|[xV\017\343\307r\347\365\226\017B\223,\035\227^k>\364fx]\257~\2239\217\327\253xL/\263\011\333b2\366x\230\2122\372\362\035\006\000jo\372\376\260\354y'\034\345^\275\005\356\363\316\375\242\347/\346\301d\344\307\332\253\003<\005\220\330Q\352\371\036mJo>\232\307C\377(\010y3\213\202\277\264\312\2074\332\037\271\007J\016\337#.\013\217R\276\350t\017\336\274m\367\376v\330}\335P\201\275\360\303\341\305\324\213\337g\375\265,\342\"z\333\014\306\226\247.X\222`\036\322C\217\313\265\254\255\364AV'{\013\242lUdu\322\260\261?`|\245\336\271\224\305CiE\2547\271\323\351\2260\006\350#\347\002\315h\330O\306+B\020$\331\243fa&\014\363\207q\303\206\262|\210\373\334\3551s\222\025\021\017\2351\245\254\344_\253\302\351x\361\344\272\"\260\0344*\240\274\341\177\346A\354\233\254\246\336#\177.\336\361\313\3445\236\226\242T|\247\222-\307\272\033\034\207\372\374\233\353\377\231\340\261\223\030O!gG\373\365\0103\312\317et\223\301\357\274dw\367x\214\331\266x\034-~\264\327\310\250\255s\304}t\3644\004\022\372l\177\226'_\352F\351\000\226\264\253p\025\275_\025v\320\332$8\277H%\226\253@\316\"9\356\221\324\207!\015g\366\323\232lU\001t\027\326\265\032\263\215l\254\222]sT!\033&U\354j\007&#\227\225OPC\0309e}\365\354e\325\200\361\227>\336\277\200\267\2216E\352\343y\342\357\346\340\251#\177\356\247\007\363\030\355\324\201\237\244\270~;Z(\276Y\227\2752T\360G\010\346\010:d\034\033(\356\215LvF\365\223\236\317\314m\225\000\302<\325\325\270\215\354p\202\242\330\335\032V\211\325\362(?h\201\275\3075\025\312\312Y&\316\002\346\025yl\377%\254\025{\305:\351\236b\261\364\346!\254'\251\377A\304\324\263\307G\336\177\257\255\241%\3438\240\235\321\315\035\226\373(\3363\213\231\350q\315\332\012,\207I\371\032\363\210L\335\324\362R\336\011\275\227?]\320\365\317\221\005\355\273sz\227\345Q\016^G\015b1K\032\304\342\"\"J/\374\270\270\332\206\262\031F\212`\040\323\215|\024M\012M\3446\242\004\200\250\012\316V\012r\362\032\266<\356[\230)R\233\005\355O\370S\246\030k\265\364\251PdP*\234\303,\032z/\336K\377l~\376\002\300\274\317g\204R\212\321\325.\227\037\314fns\376SZ\323\245\353\324\365\362\326\267]F\342\021\023\325\030V\030H,\023\243<\326\246(\020\007U\344\214\263\262\234\211\377\232\271F\3445\016o\030G\324\224\311\211\374r\323\306R>\203\315\220T\"\301\232k\366\345\347e\220\314\242\204*\237\273\273\257&\336y\302W!\345\205\312\362\3773{\350\330rN\222\007*mm\371\034\015rn\231\013\267c\216pY2\306j\2567Ca\256\344\034\324\035\204\345\256A\035\253\\k\225}\234Fmap\224\252\013\205\232l\001P\271\3540\375\263\235t\270\366\247\246\213\250\250\274\026T\311\027\\\330\012-\006\3358\360&\223\376\373`6\253\200ON]ur]\226\035\255\211t\265\0267\217\255g\357\265\353\017\023\274\242=\034\372\346\271J=\350\306\342\330\340\232\212\320\037\354\225\367+\327\235\305\301%H\325]\363\340\277\206\354\371$:\363&\324\013i\325$xS<\305\315ME\241\255\325\3320\224\036\271s\"s\014\030Q\301<\356\031\012\323\022\326<9\217\322\213\040\261n\231Xs\363\010\240e}\024>\241\335\\\247\215n\361\202N-\265`Y\365\240\234\005\265\011PeH8\356\037yLT\206\005\003@\316-]\215\324\\\177\034\203\302\330\275~-\277\2362\327\365z\332WL\370\204\340q?CU\371+\325\005\222N4\363\344\177zqu\244\212\350\241(\271\323\350RXH:\225\270\356\025k\331w\262\244\264\012\350\026\202a_\223\312*2t\333f?\313\246\330\245\005Sa5\2172s\334\034*\201\250c\223\314\213\266\000\232\244\252\346D\0273\215\356\262\341\274N\301\336\320\274#\216\350K\020\216\374\017\326E\234O\206\275:\252\252\332Db\266\261`\232\351\350\361:\305\247:d\212\222\252g\201\262\360oLH2H\226G\035\276'\374\001\202\242\317\222;\366\207g\2217S\024\351HQ\040\217w\232\274I\232X\245\336N\266\246[e\232\023n*=\260\356*\275<\354\343N\010\313\304R^\206'\3739|u\010o\017\217\273\270\373\244\247\210\2219`D\311\037\035\350CCKp#\323\034U\253\272\376\222%\026\031\221\263kR\005\253u\271Q_\270\223\366\252\335\037\300\347\267'\207\320q\014\276)\355u\347\207\203\016\275\202\247\337X\366\252\267\322;\340\332!So\256\274k<\354\223\314|\177\2649\237Q\023!\230\260\315\346\200\236\003\212\246\2604`>\356\011I\343\353M:\310\364\256\307^\347\357\247\207\275\316#\004G\205P\262U\224\214i\320\373\261\350\025\375\327\021<\031\323\354<\364&\005s\363L\245\337ZIC\210\345\202\306\270\326\217\272\035\366\206\215\371\015\221\306@\005\313\212\336\026)\206\274(\003U\373`\2406&\340\253f\330\312\307\266\2100\235>\340\242\254\0259\347F\226\031\013'\374(\322\362T\031\323\345\360u\367\270\327q_\234\016\334w\355^\327Qf\232\226\333\211\013\246\2343\213\222=\347@\311\320{\370\320e\302$\001\223\317h\373\350\260\3339\354\276:n\026\317a\025\235\246\325\215\243\340h\341\0403CW\256\241v\217u\034\332\353\273\375\323\223\223^\247\337\207\311\272\240\036/\350\236\264{\235\356\340M\247\337\351K0FU+\205\266T7\212\024\372\"\026\010\323`>\3273]\355-\352\310\361\311\242~\334\026NX\013\202\222},\204\345\323\241\274\336-\271\272\010&\260\2629\270|6\266\233\304\241\356Hz\277N>\270\224\350,\264\207x\313\237\272a\264@\267\343\275\370\024\264\272O\330\200\342\343X\034\301d\305M\341\221\207\374\2232\260\3443T\270\012\3163\361\315*c\003P\006\177\317ffT\025?#\267\305\366\014-o\350\306\275\345u\032\245\200\213\036\2155L\322\021L\352\205\301W\354<\240\262\217\312\037\310p\215M\324m\330\263\362\260!q\262P\326|\374\274\250\252\262\301\315v\301\331\316/\355\207c\217G\202!\224\256\311\202\367\307\357-\261\320*\374\031\005\240\037\001P\337\217\351\001\266E\357_\314\323\343\367\371C\001\"h\237\216\205\032\270\304\237h\004\345\317\312\011\312\012\231\004\265V\335\31357\362'\251g\226\236\305\376%\177\222\247\024Fg\261sz\032\001\370\000f\347Es\257R?\201\0056\0211\037\267\371\303\017\222A\363\321\202\013\330S=\366\232\361\250\3624\037:]7\344\317\022\207\341\262s\230EGY>.\036\225Jb\0361\005\033GiH\364FK-\246\202\010\013\003\027G~2\214\003*\326\367\220\004\0172\032\3542e\343*\230\340\334\303\254\026\250_\200v\225^W\013s4\346\215q\032\330:\326\352\211\347\034\027\"o\267-L\312\016\177\313K\361\016\303>&A\035e\314Z5t\303\020\302\246\014\016\2464R\326\020\301x\267O!\217\213\233M\370i\247\256\027F\011C\256\217\027\267u\330Un\030<<\0078\337|\355r\341\040\352u\022h\025\007\203^/\007Jg4\2313\257\205^G\011\346\032\040\232\012uE!2u\303\254\371\342\030+\236L\026\372\255\006iI\204&\336\014$\254\322\0215\002Z\357\204\245\336[\274xv\271\212\023L\327\232\257\250D\236\026V\356\347\3531\236\331[|\245\264\"\342\370\370W\227p<\006\221q\025L\337n\024\036D\263k\214\254)\210E\344e\035[2\200\3008\374\360?Q\330\220b\003j\"\200\302\202\263\030\346\357\205OW\020\314\026\303\031\234\037\302\241\323\371\203?\234\343\331\014X\024\302(\255w\022)\027\016\254b-\202\020\355\361\322\256.\222\370\014W\216z$\271\360E)\032(\365G\312*J9~\312\006\250\312\244/\260\205\372\235\003\2148r4\313\246n\354CQ\314\303i\367\264\0176\255\021\356\240\354f\0136\341\243]\346\003P\316\033\206\336\304\0255\033\231\177E[(\012\015)\315\037R\334\343\202\040\211B:\276\374\021\320<<\370\343\321\323)\271<=\347(l\010\037\340\022\204\267\255Z\245\251p\244\377\336b[(\305b~o\236{1?[t\364c\352O\243\370z\221\360\033p\315Q?\263*\236\212{\372\364\267\331.\001\036\224\232\240\364\251\\\320<\033K\217\370\026T\027o_y\030\346l\274\034x\347\355I\340%\366\252\2057\016\352\321\347H\345\371L\242i\024b\367\311\231h\234\244\261Hs\223\\x\261?rgi\274o\226j\355\231\231i\344\235\210\312\332\257\246\245Q\356L\264\234\016\317\221J\314\033\352`\323_\225\237\264\316\015\256\002\311|\265\010\2221\006*$\343\325\002H%,\245\000-)\345\330\257\212\023\015\024\215\264\002\274\250H\036sCo>\314_{i\035b\333\355\230\005\007\341\305\245\230bd\0133\3254\255\354\311\013\214\331\223\322d\011\242\245#\3747\304\226V\004\020Y\311\221\342E\336\352\013\337\027f*\222\040\344(;|\010l|\362\210\244\362s5\260\2345\255D\365\360\215\365d@\352\235WHb\264\270y\223\317\200\277\302\210\311\377\202\311Q(\314(\337\226\\\256j\315\233\2201\251*Ar\354\356R\214\334\040\345fS\2206\253\026\354\204#\262p1([\311*v1\233\300\226\225\254\332\214*\302R\344\317/j\323\253\234\037\263\020\222rUN\3710\013\333Zs\256\331F\\g\345\324\344\365\006,3\0334\262Z\005\264+\222\224\250E\263I\236\315\024\275\232\352\241+\024`\367\313Z\312\205?\213d\235v\014\307\364Q\362#\232\353\353\266K\324\345\026\230\365\364)\232iA\212\247\2269j\205gT)}\240%\334\347\311\254\030\207e\201dN5\345q\3038\351\245\356*\0256\301\373\341<\012\322\306fK\031\204\040\305Sa\034\303<\340[\373N\035\345\026\377\2035\331\205\322\234\225{\260\232\255!\313\325\362\012\037\315\342(\245{\200\273E\254\345<\2624\307\371Z9\352V\220\313\244t\236\226\315\365\3123\313\040U\321U\357y\305\315\222~+\364\257lH\355\017Z\326\331i=\353w\373\361B*\363\012\303\323\022UA\372\012\352\351\011{+v\276\274>:~\321>\312\035\336\221\272\304\215\360\233\224\256\0045\\2\246\325\004\265\201y6\364.\323\313oj\372R\226\"\347Z\215\321\324*V\033\375e\350\242\254\200\215\246\216k\221\336\253\372\204\014\227\2207\003\231\362A\367\000\341\344G!\022\244\211Um\314\236\260s\270F\372d\012Qx\275M\2771\365\360\212\233\326\016\344Ek\207\040w\034\276\273C\267As\033\250\2007\030%\301\210z\272\321\303\215\263\177\352\305\347\001\010t\376\267\005ZJC\255\021\315\323\315h\274\211V\025\036\360\364\250a\301\372\211\003\017L\300\315%\250\2140/|o\244dp\022\031c\336R\360\002?\326Xc\001j\376,\011&\040v\234m\262/\277\354\223\235\373\300\257\303\300\013\004yk\215\275\005\367\364\361\221r\304\266Z\376\304:\237,\274\340p\236\244\321T\323%\371\033\261u\260i\331\264\267$yV\257\022{\236}\244s\022>\002.n0fVA\220\270\362\232\303\000^\310\374\3172\333>\374E\010\255\"\234\234\306\302\014\025\274\006\233\013\216\026\270\303\232k\025\234/\347\263g\352\212\001~N\344\347\202\222\234U\261\040\373XP.\031z\023\237\026\243\237\25493X\341\322\003\374\037\215\352\006W\345i\276+\336\024\223\267\374v\364\217\326\025\236r\317\222w\207\251\325\003~\217\233\230\026\352E5:c\321\335I(\353^R\2137\337o\274\331\322:\264\000m\253@LRx\215Oc\324\213I\245\023\207\322KR\317F*\336o\001\344\371s\207\335^e\277{\364\323\351\354\003\363>\207*|\301;\373@\355mvw\330\247\336[\313\320\016\352\366\366\263\030\333\375;\214m\321lg\327\200\201\200gb\361\327_Iv\373\335\247N\212%\007~\242\364w\337J\030z\037\323\347B\212\326}pE\353\263\344\212\326\212\270\242\365Yq\005\353\360\206\320\263\025f\000\305\237\353\342\271^\353\272y;y\311\276\333\325\202\014\216\241\035(\312\276\001\251Q3\253\332oF5\246\365\352Dc\006V\021\315X\215\305${k\032f\234b\334|\323\301|6\364\242\352\277N\256>\265\015,y\366\270"_buf, - "\315P@\037Z\255n\2775GI\304\342\\\026\005\207\211q\313\233B\362\215i\373\310\027\206\265#\237+\231\003\321m\227\217\370b\276\010\325_1\241n\361\211\010\210\027I\303\014\373\220\254\257\023\327s\350U\304\231!\314\311T^G\206$\262{\214\361\037V\325\202\236@fmM:Y[k\"^\226R\365\255\367\336\217\367\215\014g\254aq7\227\305y\005\003q\211\267\321Y\353i&\030\342U=BV:\206\314\343\013\264Y\030\2770\230-\012\034\012\242d|5\252\223E\226%\016\307\346\222wAza\335X\267o\252\317b\177\034H\247zE8\303\013/\266\325\0042\324m?\231\217\353B\241\255[\352A\011\030\3040\251\321z\020*@\250[(\215\216\242+?>\014O\360\210\234\006\012\200\020\333\256:\253aoUTx\362\344\001\351Q\231\221\020\217:\272xA<\210\026\315S\310p\021\322@<(>\240\204\351\352\347j(\271\036\265L1+<\004cH\301\207\3018\304\033\016\255;\253\354\264e\257oJR~z\314\345\007\347\022K\300\246\372j\311\030\217\354\311[\016\214\217W\366\002\275J\345\331\231\333\361\371\240%3\036\323#u\223\311\361x\257^\225\360\272n\225n\224b\025#T\200w\3444\304\232\243\027\236\316\202\206\267\233f\314\315UpX\206\336\261'/x,.\2139\263\363\017\265\313=*\3011.\013Y\004S\317\036\\Q\025+\331$\226\201\"6j\354Y\213\346\217\017\235\025\\\337C\365O\266\221k\314\243\241\007\215\215\006\034]}\013Z\334\013\352\302\270\302lr\327\0362\374\351\345\240\364\031\031\005\336y\010s\035\264\036\274)\256\274\004(c\021f\025^\337|\027F\341&\357\306\346\010H\236\245--\346\277\343\263\177\003\335\224\035o\225\025\375\370\255\017\253\313\310P\272\265;\227\246,*\200\303\221\201\200\3619\261\022\355\326\270\216\320u\217_\374\365\300u\265-\2427\336\360=\356\015\341\372\316\346\364\3537$H\022\260\232\037\356|\363\315\316\026\236\250\230\214\36048\201e\037\323\225\322\353\022\201\034\014\017q\333\360\226\012\025KG<\3638\255\303\313\316\"\272mH\017\236\203ZyM\346x=\307\024\354Y\017d\337<\304|\330\355\336\001;\205\016@\022\222D@\271\367\001\274\005x\331n\3444H(5`\265\0035\340\014\257\211I\324\224\253\371a('\373~\267\3178\350Q\253\302\010\310\302\245\304W\022\331\326a\303hV\201\227\012\271\210N\364]\313\354o\032\035\246\367/\347E\012\025\274\370R\277\025\\\205^~\0225\203\204\362X\203D\357\311^\002\022\025\323\032\244\007$\177\264\367vo\341\022c\245\031\355qF2Dl\237-\025:#\250\014\200\357m\363O\211\2102\343I\306\030\037\306\357\307\246\015\321\300/\261\002\027\006\371\340\3357\017D\216\206\026k\036Zl4\026E!\031\227\017\025\307\032\361\362@\021\277\350>\034\373YOEX\227\364\273\350\214h\256\240\362r+\366A\265\272\0045\357k\362X\241\322\026\352VN\343\321\263\247\266\350&\005\000\036\177^w\310z\276\020\035\305q\020\323\371\232\357\363\235F\312\341\200\213\307F4\\08\013#\314\314.R\261\271n\007d\224\225<\224-\355U\270#\327b\303\322\034g\241\302\341\275-\0244l\242\025\312\032\376\336\220\023y\372\323\321\032F\263k\207z\232,=\303\227[\012'\341j\357\236\301\342\347\220\215\334-lF\277\260ni\207\264;\016\354\330?j)<\244\011\255\245d\026\312\326\002\231\365\211\010\255ed\226}>\026\212,\313,\372\"\263>}\231\205\376\315\317SdQ\215\306\020Y\246R\363G\020YK\311,\252\305U\221YY\351\022\302R[arM\375\332\214\341\033\224\345s\317\035kQ3z\376\216rR\204\040X\332\337\342P)\244\305C\262\204\264\022\341\363xa\356:\225=y$\212f\320\255\315\251\220\247\266\005dU\206P\257y\322\014\014\2651\334\246\252gp\3307}\265\026\034\232\353\236\316.\374@\253\333\356U\254\200\2630e\012q\256d\332\224\341\314[p\350\2164\303\031>\334\001gi4\025\342\234\031Q%\210\0110\016G*\037Oc$\274@o_.\013\206\360\015\256\255\261C[\3717{\374\215\370\276\273\213\200vw\025\354\253%\327P|\232\005\336N5^y6\253\354\324\314\036\312Xi\224\\\306\361\"\236\024\360\255\\\205\371\021#u\024\330\016\256<^'7\337T\277\226\274\007P;|T\320\214S|\031z#\363\322I\240\216x\267\246\306B*\"PG0\273R\324\024F\322c_Ap)\273\203\026J\256\255\311\217J\254\277AL\221\200\261\244\267{\305\254W\203w\014\016)\342\243\361$\362R@\002\335\347\025\231\310p\215\277\342\020\324%\320\017\347S\356\211\026\257O\320m\3667t\202\355\322\0047\337\271z\3322\272\310\342V`\020\266\317\222\214\367T\246\343\033\365\306T7+\211\030o\350\356\271\2376IQL\272\3010\274\230v\223\250\237\347\200\332\253\335\236\342u6\202\000\264\335{\206\356\236\375\245\031\013p[D\273\323\311l\011\342)\265L\352\311\214Is(\323\264\014'&\335\2449\301?+\312*\211\240\260c\372\313|'\247\256~!\351\255\236e\350up\351\207\344\260\323\351l~\373\347\257\321\222\201^R\2471\235_\011\365q0|\200\206W>\315O\012\263c>\365U0\324\021LkdY\231p\257b\032\211+\270&Q\222\000\214d\2137\231\322-I\006P\205\304`3\200`\311B\201Q$v\232\303\221\027\343\011y\3642_\372\324\343\314B\247I4Va\374\212Ae\233\030S\366+\3066\212\370\375G0\202\037\234\261w\226\320\260\274&\241\0371\020\015>C\023!\266\006\364\326;\006-\343\012\317\362\213\252(A\2472:5\001\361\374K\025\022'\"\353\331\225\037\013\372\370#2\206\017\034\316\226}r\364\374I\375\271\221U*\020,\2765\014\355s\221,\271\370#\301\332\306Z#\346\204\334\370\037\300\230\216#L\373\212H\216\371\031\302\204\014c\037c\300p\310\275!\335v\020\346|D7P\370\340\210\334\\4q,;\023\204S\214\3362w\346c\264\006t\222\347\036\024m\357\356\346\205\034\177rtR(\270\200]\241\302\313`<\346CT\015\034\233\207\365\241)\253\227|PeI2\341(\314*\037\024s\240\014\005\021[\255\211\234\260@\320\235\355\355G\260$\343\226\227K\243\"\022\301\362\240#\361`\311\306\222X\324A_\247)\373\266\014\362"_buf, - "\264\346r\270\253(\254J\325R\224\250\"=\213o82m=\213J\020\323\306\233\264\252\245\322[\240\326\277\346\373\2327k\266#\210\246d\031c\360M\360_\377e\346Ar\230\210\321\302bP\0005h\200\207-\302\263\305\315\207\023\230\2554\313^\251\3350\320L\005\321}z\203\233#<\026\015t\332\314\004\270=\213e\241\271\2744\353\302\304\302)oG\230\005\023\177j\015\276\241\015e~\305]\005/\006\031\257\021v\260:\254~\212\225\242\040\350\210xC\033\265\031\374B#&\013i\015\200\364e^\034y\220>\303\017\253\360\231u[\303\310\261\001\315\271\030om\226\020g\276\225\256\0204\207{L.<\2720\210\023t\223k\274\244t\030\214\257\331-\335)SnD\023MXA\350\336\273\200\303V\023~^\264\204\035`=\272@\345\3109\233\343F\177\222`\040r\003\2657\001)\366\3773\017@\213!\036\350b)n\350\017\336\2360|\013\375\031\234.\273\273&s\342l8\321\371i\021\247\312\341-fW\3566~N\326\327\033\371\301,C\306Q\240+\2402&Z\205\260T%a\221\264d\275\262\207t-i\246\366\323\021s\037\252v\252H\012\216\367\006\260\267\331\361J\375\256\314\254DQ4f\223\026\352\373!&\316\277\364ww\017.\242`\210\232y\3664H\257M\007\252\012\314\033\3415\005\345\315\330c\014\264\262z\203}\032=k\011\202\022\375\312\243\214!I\032\214\342\306\350\035.\245v\251\210\334\266\204vd\236$>\270\006\325sU\255Da\316@:\355\225a\022o\231y\303r\001,\245c\027\361\001\215\216\024\260\313\010$\361+%S\007O\262*\246{\236l:m\264\362N\275\236\253+\214\215\313h\3365Rln\334\356\345f\020\217\307\256\216\277Q\3437\357A_F\302W\357C\256\316o\336\213\016\217\247\257\301Gz\215\217\337\003\263\013=\377\334\377`7\323KD\205ZKG%\3067K\012\347\005\335\343\246\261\253Mk\246\234\011\\\212g\267\034X<\322Q\\\344v\255\372S\032B\317\333\256\040yJ\350t\373\333\207\323\337\253\340Y0O?\272@2\355\372%\305\221n\356\177\216\302\210\032\001\227\201\007\2058\264,?\005\335<\026[M6\320\040tD\313[\004}\301C\001\221\272\235\351\261Yz\220\254I\374\255\363-bWI\374th\307\233\356\222\323\033I\207\330{\206N\020C\375\311ue\321\374\340\371\335e\263z\3307\240\207\277\340\317\276\204\273G\036?\016\032\305\341\361\012V?\005?S\214\340\357\262\307z>\222\324a3\351\2562\347S\0269,\261\305\235E\216\006\306Y\330\311OG\344|V3\206g\324\244\023\347\023\2379\350\304\241\330\256r\362X\266\224W\237((\307\31653S)iGE\275\302\374S\305)\203>\235\316\327H0\245f\\e\265\212\322H}\016\035\257\234)J\3516\253S\220\034jQ\247W\261P\250\231\003\3549\214\030\246\331\015m\372k\274\034@\244%\276\367\305\3474\214\342\221\017\272\333\362\212\257\332o;\270\305\253\021\017\360a7S\320\317\216xts\273\354\362\263A\340e\345\005\210\265\247.>\376P,?\345\327!\324=\367,&\310\314\217\221Ohd\211l\377\314\307I\333hJBl\371\341\010\277#:\374\345\375\036\3521\306p\341\352!\020-\\7\212wH\312'\227\031\010Y\260\005\302|\252\277\311\376G\305\251(.\007B\230C~\241\203uzB\301\214\020\264\005\326\271\334\036\207F2\235\365\215\335\217j:\040\261\204\236\324n\332\261\354<\336V&\224\215Hv\032\350\316j\305\202c\0053\002\014\226\353h\021\374\273\365oE\214\240\011\326\0226\320v\340\356\316\004\025\233\375$H\244)\021%$j\313\354\370+!Q\305f?\011\022\331\327\351\022Z\031\025\252\257\350\205\344\252\213\202\303A\222\325\306\267\311}\276\202\374\215\366\363\306\012\271y\253\303D\"\220\227\300\205E\035\206\210l\330\230?ErW\312\327\373\276u\354M\347\340o\356\340M{\200\234>\214#v\301\230D\227\235V{\031$\263\010CF0\302\224M\027vO\326(\"7\312m\245|\226\231\254\302\326\007\363\251\322\340\303\207.\317\230\211a4E\327\251\347\356\360\352\035v_\037\276\372\221f\245!\353M0\252\012\213\360\0165,=\312\256C\303\237\334\215\\?j=\244\353\221\2557[b\346\314by\345\274\301\235:#<\344\237\224\276\323\333\347\224\306nMl\350\277\216\035\001\322(\356E\257\323>\030,\252w\213\031q\3215DmovS\330\307\271\363\356M\357\370]\237g@\355k|\230\3358\013B\300\312\215\222\236[[[\237\030[\272\356\367m\267\335{\335w\335E\354\251\365se\274\214\313\227\235Y\251%>\300K4\023\247\241q\016\225\304xQ\247\361,K,\3162\306\343j\321R\273\2507]e\256\234\206\376\207\231\317\024\003"_buf, - "\336\375n\224R\254\360\222f\003\332\255m\026::\207d9\016\320\250\250\211N\331\324\305\010\273:3\267\004a\306\2477K\323\3520\034O\202\363\213T\275\317\272\244eT.j\310/J}X\231\016\200C\372\357\203\331\314\0379e\362qi\311\242\330\015\226\024\307\330\004\231\3204\2500\323w\212g\321l\216X:\026\270\252\357!\177\225\251\342\020\244=\035\372,\263\272u\232\330\330Zo\2373Y\365v\224\245Zg\241\206\275\271\374x\234\266\012p9\265\343\222\341\2527{Z\263Y]Y~\235,@c\356\037\307rt\232P\2360m\031\017\037e\\T\210\254\005U\015\242\031}i\257\010X:F\203\250`\347RY\233\2028?}\315>g\020\235\212]\323xB\334\031\201\314\036\215\2412@Ym\227\226^\2243,\177\222s\360\347\255\234\230\257\263F\363S\034|>\267\236\353\264(\216\356\255z\335\007\353\030\273\343\2245\342\242\3727O\310\363\302\016\231\013\216<0\252C\260\241\205*\007/U\206\225\244\250\350\370\276\255\337\225Uc\344\260AR\260\210P\026I\347\263\011(\267\011\224\004\351J\203L\212t;\245\274~\200`\004\012\3555\300\340\347\0028(,\230\330\242\356\330\032bi\336\221\225\264\345\005\343z\275s\231\177\216\336\353\306\317\362y\212E\230\370\260r\205`\300'\027\364\014!\277\214\030M\3559;\363\010\225=\236\316\016\226\040\204\267`\335\3456\262\227\200\316\263W!\320O\235\3602pD\231\365d\301\322S(\362X\240H\366\375\377\263\367\355\375m#9\202\377\373S0\356\273\014\345\310\212\345g\374\322\216\3438i\337$v\306v&3\227\315OGK\224\315\216D\252E)\266'\355\376\354\007\240\036\254'I9N\317\354\336\365owb\025\253PU(\024\012@\241\200=\227s\210\366\331y8\361Pv\265\330\302}\371\321\376\220\351=\322,\252\0068\307z\360\243\370\373\360n\002e\307u`\236\327X\216\027\270\216E\260z%cd\355i\372\316Y\347\224\243\234|N?\374\300\231_\030\223\377PL\376\001\323/\246\022\365($\244u\021\246\270YI*a\216\035\347\024\026\353m\222\306\307\351@\006G\030\212\3374G\353b\355\251w\253\277\355\010\206t\222M\343\0352I\021[\212\006\230\376x\231\342Z\336\304\311\244/\003c\006\177;_]io\340\315$Z\002\306\321\244\010\226)\256sb\014\267)\034Kg\370\3409I\345\363\005\314\006\011\312=\034\220\315\340\006\2373'\323\200L\231:\024\3612\021\371o\360\2178\272n\025x\343H\211\037\214\250f\360\266\220\012\244\251W\016\231\320\010\207\300\020{\017\305\2210\204%\337\223\245\216vp~w\302\006\223\022\324c\202\331\324>\310\0342\027\314\272&W\241\002&;\204\024\351\337^\340\200_\014\002T/M\031\010*hfwA;\372\345\035\243v\353j\212\000\242\226v\035jj]\312n\323g\017G\243{\256n\351J\015\004R0\337}\315\261FQ\300\214\276"_buf, - "xKJ\342\302Gm\\\015+\275r\262\223\275\2502\337\275;n-\273\377v\337\350\212k\2327G'Gg\007\027G\241v-\303\357\177\324K\344\202\264\015\217\021\356\307\364\277\217B\343\303\207\223\343\277~8\352\236\034\274;R\317\224\246\303S\306\372\317\353\234U\243\355\247\340\363\267\300Lhc\315h\327\307Z\255\353\246\006\040\360\344\024\206p\021^e\331\3250^\276\234%\303\3762u\261,\273hX8\355\036\236\276\377\307\177'\304\356\377\273\040\366\354\350\365\177'\274>\375\027\341U\277;5\274\261\274\276Zz\340\367\262\030o\366Ma\225\021\367B\235[\265\021\313e\256\263\354\270\232\331J^Ci\227K\362\263\372\234\3224;hc\013y\023\021\370\277\312\3245\247\011\330|\264\241\277\367\365Dt\346\267)\374\011\320\023\230!&X[<\032\215\247w\305P\230W\012\212H\3444\032\367\027\2356\246\357\263N\264j\031\236K.\015\304:\355:L\025r\015\311Va?\242\232;\334\\\021c\213\271\301\345\206i\302i\216\200\375q\314nW\213$|\212\020\040\257\244z\354\305$l\264lvu\315ur\023\222H2\323\233MQ\253\236\220*|\023\017\207\266\005\204\217\3205a\215\274\335IR\234F\020\016\362q\354\373S\330&\363\356\216\207\331\316\265\015IFs\321\237sO\271M\351\201[\321\223\241\344\365\033\317!h\037?\202?\311\356\3544\021\325\326\317\340=,\2671`v\235\250\0157|\377\224\005\333\377\341\334\312H(!4\320\367,\032\177\303\307\277XRu\245\265\305P\234vH\334\210\300\273\321\207:\270\306\351\244\314d\205>\0131gv\342Z\224>\214q-\341\253\013\322\200p\326\012\2169\020\014\270\212\033\223\271\002Fi\341\237\336r\347\216\005\355\244\313\015z\354^\023\326\306\305>\344\204\255\006\276H\012\232SV\341)\350p\011Z<\311\270\373.\033\301\040\003\026\205\252.\233\035\260\007\211\327\305Fe\362\336\373\3077^\177\237\301\232\252\324g\324U\\\263\256\035\233\334\316*)\224\362\244\226\015\256\201/\016\321\011\246\261\373H\274\330\317\266\\\374\207\321@(k\326f\012\365\230t\371\245\270\343\376S\343Q\350\374Q\314\301\270\315->0>\342\343\363\325\214^e\352g\3618\216\246s0\365\207\373\256\230]\025\241\003\335\016,\206\217J\231;K\240;\260\2502]\355CH\004\344\261\274Yla\314\020\230\241\006\316\254\344\236\230\327\360\210\335\354#\260\301~\\\342\036c\240O\010\031\274\357?H\006\027S\015\371\277\025\302\270\331,\350p\251\234MF\021\030G\030<\221c\011\376o\030\243\033]\226\366\342\305\357\277>\324\256\370\344R\000\242\313\245H%\267\271\315\350\274A\243\224\366\324\264Q\353\032N\264\371\244S\303\347\207\036\022,\312\015\027\000V1\245'li<\315\363\035\263^\273\021|d\365\362i2\034\302\230\242>9\033:='y\243U\331HT\317f\023rw\355E=\220-,]\201\031\232\007\311$gR\005\345\270Lc\364\210\314\202q\366%\366\352\022-\207\336\2210Y'\001\336\201t\3132f\342h\206\361`\312\254\332\321\224X\025tD\371\252o\022`\035\314\244\215\356\334t\301G\372\275\030?%\233\304I\260\011<\230~\036\244M\3259\250\215\333]>\2169\202JYS\330+\341`\016=\261X\306\234\334e\355u$\017h\040\334\004\346\020\360s\2132\245\002F{_D\212OX\272\257q\372'@\375,\015f\343\040\272\302\220:SvA\301\023s\351\312p\261!\334\012\261\312>\367\325\035\350\277\276\367q\335y\260]~\245^\007\313\337\243i2`s\037\003\017S6\215\303\207\204\031\331\345\367\353\233\312\305b\360z\226\366\364H)\3439\304\023d|\030\234x<\311\276\"[d\342\012?\217(\342\347\010\031\207\022\367\253\311\322\325N\022\240=\340'\364\316C\244\356\366K\021\037O\\F'\177\202\215\271\354\212S\375\036\003\217\350\302}Q\325pp\270N\343\242?#\310\3030\3467Vz6\301C\216B}\327\314u\024\212Y\365@\243\237\230G\236\266\334\374iP\040\236\305\030K\336\277mx\002\363\316\261\340\363\206\343\235\177M\234'\256\036\307\375\301B\277\311\350hl\225\251%\344\031Z\211\031\325\244hG3f\015\350Oo\315b^\005_be\366;\330{cj:\276@4\250\234\254\336\302\025\330D\304\022\321\347iab\242\376\014\032\345qI8\012\012\264L\350\337\252f\034\015\016\034\365\330z7\224\327GV\000m\256\367\232HZ\342\306\011Id\222\025\223\376k\326g\353\301\232\370\221\307\333\322\300\221\317\247\037\306\216Z|D\254\026E\3326*\211\365M\322!\205U\253?\024c\301)\244\202\321\034C\346\25337b[\332\265]#\024N\271\344]tr\364\367\013tt\012A\\M\257\226#\020O\357\376\031O\226{\331$n\001{\224rA\277\205\236\217\003\246\0355L\376\273T1PMf\026\250q\340\304\207\014\3519Y\276\216\367J\324L{\015\265\347\344p^\305\357{Wk\253<\313gQ\3604\230\260\210\245\367fL,\311Z\375A\256\030U\267\256-\276\234\261\013g\217\277\225pO>H\003V\021\337l\230O.<*\306\251\032\207\302\024\325Oy82`,\351l8D\234\375\215\371\235\343\317\361\224g\2761\252\027\306\006~\377\255\006\2775\341\3047\350\301\236M\242+d\"\027\241l\0238_\301\212.N\265\2508]\262o\227\365\303k\374\207\243\303%\321\232\267\242i9\215(\277K|\230Rb\356\275\315fMDB\367l\262\037\370\006o\335\330\204\301S\376\0154\014\012\252\341\276\2746:W\001X\210Q\377\323PDN\012^\324\324\020\360\214L!\367^\014\004\210"_buf, - "\002\213D\252PZo\300\002\332n\255\021\352/$y\227\216e\320\373\265\221\251}_\356\374~Q9pFf\356[\275\002SK8\0329z\015\310\256\313QOm\307\337\207\327h\275$\333-w\324\016\275-\030h\263\235\321\241\325Z\031.\177,\252P\001\017k\302\021\\\366\270]G\345\177\030\023\203-\254\202\362_\035\346\331(\256\0304\356:\261P\226\302\227\245\225\315\367\265\346z{\271\027\236\314\007E\306\200\274\035\003\243N\246\005\040\004[\036\026\200M\331~[k\212M\027&\261\024\217\035\206\370\0360\017\351\337l\020^\200\222\211Q\204\304\036\374\304\236\313B\371g#\236\034\036\203\345z\214r\302)A\231\256'Y\232\251\361\227\222,\037\334\364\325\022\036\362\2724\270S\225\006\023\247\263\021?\017\377\026O.1\364\352\235\202\304\277\316\222\230\204\360\302\034\177\222MF\321\260\370\375sru\355\222\320?F\223\364\3402\233\341\372R/\037\361\002\360\233\002\007\230\022\305=_\271]\321\340\313\330\2339\373\330V?^\304\371\224\227\257\362n\315\236\317\257\263\233W\263I\304@\360\336O'h\300)\272\177\3056\313\353lr\026\243\327I<)z9\030\336Dw\271\322k\374\225'\331e\235)}\201T\317F\304\2739Nq\232\247\230\271@\351\3548}\025\003\222\331\220\350cS\371\3666\276Mz\331\025h\340\327I/\032Z\337\317\242\264\237\215\250\330=\210\0179\010tC\274\276\346\243\370G\234\343|\325\374\001\263iV\300\204\357*N\335P?F\011\242\347/\361\035\275\005*\2261V\023\023\020n\012`/\343\001H\300\224a\036-x\346\207\243\333\004\313W\235\015\016\322>\377\256B\371Mij.8w\234\207\0058\007\335\336V~QQ\332\201\011\246\207\331\370\216\374{\276-\270\325`\3229\035Z0\361,=\302\260#@\223\250\255&\002x\312\263\006\324h\301\003\002\342\026-\257M\243\341{\373\234]\344\014fC\246\236\3465\232\262G\313\257\342\313\331\325K\314f\\\243\311\215\330\306\357\222\034\255\021\305\346\234\2471\337\267\345M@\035\010\242K\330\213\007\370\320\261\336tn\216\323\257I\216\031\344*\200kLag\207\261\203\\-,o\317\362\323\005\243$\025\015\312\353\013\242\224\371N\370\357\032\323\272\216rl\315\334\003\353P\034\267\327\251y\020\024\213\012a\376tr\021]U\300\022\274lgG\345b\223\031\343X\345mg)\276\230\247G\245S\324\003\317c\014\266\\\326B2\255\235\035\301\256f\242\350\273\347|\036\263\254^\027\031L\252\034Zq\354}\025\177\351\015\334\213t\031\247\275\353Q4\371\002\347\025*\377yR\203\272e\243\363\010\325\346z$'\033\021\233\352\243\233\000\205\323\004Yr\2165\221P\200[\324\351\234\020\313\244\220\235\235Q2\304\034\311\350\367\223\027\220\340\204\037\315\306\027\211\213k\335?\334\202y\357\217\271+\255\004\206\221`B'd\027\216\247\313x\242\330\3505cn\017:\006X\2352;\002\245v\303\354nAN\206\215\200\376\027\003;\320\226G\367\323\303g\317\332\355\340C\232\300\2414\012\330\331\034\234P\317\002\212\264c\267\320;_\372\267\012\257\024\014\017(\244\327\274x\303\315B\243\012\030\262\325\214\\\327\232\224\267:\231\212\270\023\2271y\211Dy\040b\251\363\367\333\210\336ar\371\247\\\002Jp{@k\334\017-5\013\23617\230\366e\204\357\3023\346\347\365\376\360M0\210F\350f\027^O\247\343\235\347\317\307\275\253e\206\353V6\341\2415-\303\220\365\310\232<\361\272<|!\363\221\206u\330\\\357z\036\233\261V\314\312j5[[\355N-\263\"\221\016>o\327Z\205\300\254\033\276\030_+\273\316\333\262Rp\321\255\027\234\372\352Yi\324\011\227\333n\353\010\254\301{\341\245\224\321{s\222G\305\215\"C\032\335\221\341Z\210\2572\374\244\342\224\251\240>l\354\250\277Vn\343\376\326\372Z\257\267\376A\217\211'\225(\265\266:\327\034\330w\267a\006\267\302Rg5\255\026\020\\/\232\364C\261\310A\376%\031\253\260T\010b\037\204\015)~Yj\031\2670k\352\343\376~\250\222\234\010%p\015\373\311Q>\271\316m\213\265\006\357\311\003\340-X\276\221\240j\221/\3520\317\230[\244\000\277\267G\236\220\342g\247c\264\035O\"8\260z\364\334\246\237\311}\017\040GM\240\017,\276A\367\\D%\225\022e\244\331\315\202\021\342O\340|DQ\202b\335\003\015\231\333\030\271&\017\215\312h\254\037\003\177\205\255\002\374\346\006\024\220\353`\221\011\256\213\242\032c\253\271\012\012\000\365\256\263\0344\201\345ez\"\024}!\007\333\010Y'!)J\231\303\035\343jb\022Y:\274S\341\010\337\321~\014$\177\223L\257\211\244\204\0032\006wf\203li\373&\351}\201f\227w\322}\367\005\374\230\242\327)\272\361\376\237\347\375\370\353s\306\246\376O\260\263\334pno\003_\370\016\011SL\302\236i\257\305\375\225^ocm\260\275\266\275\276\265\371\341\355[\014S\335\206\177\033\240\215\340\277s\353\370\376\003\2528\237X\235\316<\317\212]\336Z\303,\2023\216\277\357\040\220T\004h\252\343N\303\232s\036\307\275#4\033\377\250;I\345\325%{\356\202\007a\027\226b\330U\317\032\001jD\245\254\005\025)N\262\014\037\2732\3143U\362\014:d\215\243&\207r\251E{\306\253\007\272~P\034\007Y\317!\264\270l\330oX\224\314/\334m@\273\013a\235\324\363\227\260\234\177\305\274\212\323\245\302Y\302lJ\216~4z\232\232\035\013\243p*\270\247\020\325\016J@!\361*\236\350\264\300\013\353\220\202h\377\000b\200]\245\323\202\204\245R\003/\254G\017\346\300C\321\032\226W\374\371\203IBt\363_\206(\220\347\237\276:\335\011\216\221\277\202\034GW)7\\\200d\307\024\223(\"L8\241\371\342\177\215&I6\313\231\225\2232\0347\027d\034\040\224E\217\341\240B?~&\177\342q5!\227(\364\353\247\020\350\344\360\237\024g[k\301\355\355R\225'\036\345\377\253I4\334\223\251\341\361\021Q\305\3333WXO\026\263g\201\361\331\360\002)\347\242\240\203:\336\222\226c\274\2234\311\021\206Hl\201\273\273\334/W\363\005\040\277\010\214P\206X\000\007j\361\003D\364\361\356\202\342\203\311\022\207}\215\215\274\001z\337\241\274y!\005\262Y\334\304\000p\345\027B\327\330\011\037LH\3154\226\202\011\330\341\377\2652l\036\022\014\265T\2140d\025\202Np\021\2564\314K{\371|N\204g|\262\317za\217\347p:\\\373E\231\222\011\031\024\367\013\366t\214\311r\325Ws\022\030u\010\220\260G\002t\216\005\344\306Y4\377g<\311\034\255\303\260\030;\266U\"G\362\241\201d\364\033{\037\354\254\326\021\325\224\256\351\355V\020\335DwLn\203\317\213:\303\235c\345\224\2452Z\361\312\264\272\354G\260G-\202\377\000\\\264\361\356\373\002\2654=Q\304\305\003\271\2746\376\272\374=x\266\257\321\263\002XA<\0168\324cv\3224v\264\020\246\274\264B6\200\215\351|U\204\250\233k\207\360\351\350\357\202\005\367\202#dz=\212\341\313"_buf, - "<\274;X\244Xl\364&\364\222\264\017\364\355\023\2441\317\263%\215\020x\204\354\202\030h\374e\254\353a\030*G\311C\0163\003!\021\235\257x\342<6N\032\325\214\374x\312Z\316\305\313\037\364j|\321\356\252\376\233\361y\337\213/8\037\014`&\021\3566\341\216\375\255\0363N\011|<\233\212i(\317>\250\374\034\240\200\3245d\246\000k\262\241\326\230y\2267\365\226\224m\244\261\303\307\031\362*T\250\371\303!\177\240*\030\360\033\255\333\246\015k\016/x{Q&q/\006\356\324\347\221FD\340\215|\321\233-fn\336J\203WB$\317\303d\235!\221\255\000\310\354\204%\034\251\301\217Kt+ue\025d\272\327\330Y\203]\333\231q'\331\346\340\220\273(\231O\363=\255;\261U\230\201\321\346R\005\334\016\035\254]\306\263\014z\202\017&9M\263*QQ\001mq\026\2132\324\332\235\220u\010]xy\314!\013\026\025O\346\300\226l\3630\234\310\346\322\2716\235\376@\034\000\364\326e|\225\244a\243\211]\265Prlp\214<\226/\274\345\345~q\035\347\3704\005c^\343+ynp\352\007\024\"\040g\2314s\312)\321\025\306\250\2747I\306\354\352\002u<\240\022\262:\"8\0365V\246L\"\305\262\227\215\306\011&\372\313\371\305\233yC\203\327\237\250_c\027\203L\277\230\231\313\217\345\247d\200\301j\273]r\274\355v\027~\002\375\362j\024\005\364;\350'\321U\232\345h\346\303g\033\376\257\350\307\203\321\200\026\227?\216\243>\240cQf\027\254p\366\307;\322\343\364k\366\005\225y\365\013~8\204)R\004\327\202\003\221\327\3049;r\336O\320\004\247\253\240\327Q2\024\025V\365\012\357\242;\365\353\232\376\22594\210\217\353Ms@\357\341\\%\237\011^cC\257\361R\\\003\212\357\233j\202\323\302\247JAJ(\016Y=\003b\027\021\336\364\276\321r\265\040a\344d\356f\375\230\0216\276\244,oXr\275\334\235FW\271\277\271;\310qW\015\355\353\364\235\317\343)]\321k\030c~\003\342W\323?6\034\223\006\232\271\212p\002\022\247\254\361lq\252\271\264\030\037\263/\027\031R\217\347\263\270v4+-\270\360\217\243;\310Y\\\350\212\252H\013\273\316/r\315\335\237\225\265u'\247\262\360U\243\332\020Y\326\205V\327X_\261\256J\005s\253\343\305/\377S\277\034/\334\227p\205\013\031\332\301H4\213\250\272\257B\215!-Izi\352t\3644HL\342\223\375\342\275\013\3425to\316\370\006?\006\366\262\021\331&\324\261cQ\265I\350\336(\222\037\330\255t\037\321}r\230\026\343\024\276\276\334i\334I\264\242\345^uC\337\323\031\303;A\301n\207\260\253\257\241\354\004\345\200bQ|\253R\032\021\327\201\376\032\214\216\3305\3248H\373H\251\242eZ\024\225\267\255\012_O\322\311\034\307n6\226'\251&\243X\"\200\3775\010{#\205RB\325\303;\366\230\312\365\334K\274=\3638Y\221\243\231\302\2164\327\225\022\027\224bh\022%\247/\377\327!`\304|\270r\371\013\227\301~JF\244\243\356a\321s\236R\266u\335q\311@\010\345\344e+8HE\330\332!\203\007\322Z\324G\023\273.\276\011\243\200(\345\022\0325\301\336\371\223d\236\242\001\324\345\011\335*\337\351\222\340\345\235\"\004\"\010\326\031\277dN\003\030\370\362\214B\242\300\307G\375oA\336KO\262i\326\2433\013\257uQ\250\304\313b6K\366T8\036\016\032\040\222\366(\321\012\356\221\361l2\316r\014\000\005#\357\321\300\321\267\006s\257\216\342\351u\326g\002\257x\031\324Z\370\263\354\345\264\367:\271\305\367{\013\013\177\026\337\027\026\226C\344i\015<\213?\214w\345\317i\034M^e7p\256,\374\031c\024\371\250\222\277+\352\275\243\316\025\233\210\302\013D]\353\301\021o\025\006\207\004\2457D\327\203\243\2670\226!Os\010E!\226\007\364\332\037\312C\376Qu\355\320\036\022\352\314\331|\337\321\307u\005\272\377\364\211`\223\233k\3573y\240|\336\325\243?\001[\305\213\275S\216\251sP\225{\224\240\021\0404\203?\347\3427!\256\021\230\317P\312\233\323\\\346kR\364(\326\206u\252\207\241\233\364\316\243A|\026c<\271\230Z\006\226M\302:\000$\007\221+\242#\230-\017aL9\363a\241h\032\372\341P\320\311\253\030\024\315\241\342}\314\037\363\251,\037N\307\0034\3043\357R\331\025RBu\040\365\362c$\222p\253\317\222\032\340\304q\306\005\003C\2279g\202\336\022R\3479\271\374}\372$\012U\"\373\010r\307k|M0\335\371\363\"m\244\356\377\314\341\377\026\233\306x[\275.\245-oj\035\213\322\317:\341\210=\263\217\003\341T\362\032T~.}\212A\031\344f\322\212\273\226\214*\273\357\247O\332\270\016\202\306\347T_=\317\250\270\255\341S(q\327`U?\\\274~\301J>;\037va\222l\247\215M#/\021$\354*\311\341\034Cf\304(;\017\033f\274\024^7\315\224Z\272\203(:\223\246\031\221f\214\237\360P\353\342\003S,y\213\267\323\342\331N3X\321\366$\325Xb\\\222\232\206\"\367\300\371\301\353#\370\347\354\350\342\340\370\344\350\025'\374\245\306\210\250%\014\370\353\032*n\004KE\377\012\216\035\343\340=5\225\361j\332\027.\027N\247\307bO\364@j\2245\341\347\263g\026m\313\035\011-8\364O=ci\354\207\212\263.\365\222\315RG\304>\206\344%ybq\300\335^6\276c\337\304d`\"O\011H\340\016\236\022\362\216Fl:#\230\016\253\016\177;\346bl\030\"_t1\240>\021\223\244\023\360a}\032}\366\3043Q\331\003\253L\\a\037aJ(\002\276\007\006\356\015\222\235r\344\011\241\002\247\031p\316\040D\353\356\"ex\372VK\357\327\370\324\276\002\266\225\317.\221y\004\355\215\3003&\227b\0120\030#\307\024\033\032\263\246\325YD\320\213M\203=\326\003\217\352k\025\370W\205\212;G/j\316{\251NH2\023+\304\004\013\343\004U\377\203\212\374\361\370\0311\222\311\335\317\263\313\260\321R\331JhhA\230Q\276\020l\004gd\224\206&P\251\334\250\012LH\250.x>bF\374\302V\272\276\022..6W(\347\227\037\003\0327{\366\254n\\\036\273d0\211\345\216\360\276\014\2767\257\031\264\356\345\335\005\250\017\301\023\226\334\245\317\031\341\341\351\311\353\3437\335W\307\347\007/\337\036u\337a\341\321\331y\303\024&\336\341\216\210\311\005PNQ~<\036\215\207\316\017\342lQZ/\350\324\310^\317Q\245\237\263!*\000;\242\257\227\260\236{\362t\352\330\333Om\026\2522\000m4.\301\212]\367\211\227\"\217\373\0344\276\335W\200\323\306dh\363\032\\*l\211\202\272\360}\354\304\024\012$\\g(&\253\210\271\245\040\3764\204L\245\015\302\0276\251n\300\"\253\250\350\205\021\324\301\331!\246\3749=y#\307nP\355\256\223\006\216\320\213\004\023)k\210\267\007\311\352y\226[_B\361\341\333\017D\024\372wP\310\2004\031\242?\212\\\257}V\204"_buf, - "\316'\245\242\357'\254\234\3444\255\213\214\015dG@\371\\k\011l\223\344e\034\3167\215\305\230\241\237A\331\011\026\203g\342\212V\034\017\354S2\270\253\"\312\032\213\315/\301j,\267\250\371\337l\301\351.\360t`.w\013DO\3662\346\011\352\022'\331\3645\346\222\370\303\250\240'\326\345\017\242\203s)\200US\302\271\"\254\375\277E\013\373\316`\321?\210\004\230LL\006\372G]~\027\313O\3735\327^\324\374\177p\345\251\3420N\257\246\327\237\203\345\340\223\354\211\227\375q'\004>\304|t\262(DX\335\343\300\224\034}\365P\364\264,\\X\270\263c\202\330\331a\262\303B=qB\206\010)\207V\320\233\026x\244b$\342X[\250{\326U\216\246h\374\220\361\024\314u\241>\317\255\034\223\332\374!\243\022\333~\241./\250^3\331\330\032\217AX\222\360\2247\305\366W\040`v\361\204\215K5\252\005\257O\315\343^n\210\344\235\247\207\320\365_\216\264,\233\301$\313\246\315\200y\016\235\317\006\203\344\0268\025\026\376\364\223Z\250\002\2718:\277\350\036\036\234\037\2552%\231)\307\026\220\377\\x\246X\022\335\2753\253\012\252\335\354b\316\001\344\033\374?_\302?\263{y(\270\237\003\272b\257\250\327\011\316Ft\"._J\340K\233\220\023\370\202\023s*\342\202F\031R\273\224\311\024\023\213\232\001{\304\315\236r\315)\275a\2104\343\270\237\363\224\333\323xB.R\313\203\250GwVt\242g\003\314\331@ag\350\321\314M6\371B\026\001\247A\340\350\357<\265*\345X}}pxtN\376\366\356\312'D\352/\217N\016\177~wp\366\227\343\2237\015\363nR\014K\271\330t\204\336\023#\354\342\263\360\334\0315U\275\030\345\265\035\200\304\263}\007\000Z9tY\353R\322r\375b\365\307\271x\371F\361\257\351\376&\031\366\361\3114\340`\212\013\343\270\370^\370\246\3340~\344\365\337\263\352\246\257\231\374L\017\007\262\324\274\237\311D\005\333\343L|9\230:B\370\350\025\200{\353\221|\364\317/\263\35152x\250c\002\375M\207b\271\215Y\276.\306|\235~*\034u\315\000\031\302y\234\2627\023p\024_g\370\256\272\247\224b\254\017\207[\300\357f7\030b\225?\274\3678\021\220\360\032\347\316\001)rl\271\257\0117/SX\255\004F)\356\251*@\026\316\333\256\371\216\272\306\214wml\012\362\030I\002\304K3I\035n7\253\221\040R\257\263\204\203\232\0377f\231\031F\331\021\012J!y\366\301\336*\346\005\274\026\027\241\214\322R\207\275]\322\220\244\035\367w\235j\\.R\302\252\0378\243\263x\356c\325\310QEe\213\336\234\255aEu\237;E?d\222\026\237\223;z\013\377\330Q\264\007\206q\024-\004\326\245'\304\234\313\240\300\360.E\323\365\201%z\344W\316\306Z<`\015\204VV\201[\223/\027\033\353\275\272gLmK\244\377\276\372^t\025\040\234\330\232FW\377F\310\322\231\012\214\255\0149G\267\314S\351{1d\300\011U\332\346\363)\262\320\011\330\177\030J\224\301\214\272\3268\334\370\341\314\220\005\0133Nz\325\255\265\200\335)xxnX\002\347\237\232\2375\271y\222y\306;\247\361\216s\3732\320\273\336\231\352n\237K\314i3/\341oL\177\314\005s\343`\224\241t\314\026\254\012\3331f;\325\177\330tl\275\216r#\252\233\231H\346a\270\0273\340\315_\336\261^B'^:&\274\274\031h1\270\224\030\363F?\214\232\365\331\223W\357qJO\277\016&WyXG\350\321p\214\004\311xP\035\227l\214.!;\263\202\372\350b\300{\322)TI\345\261\374X\271\262\340W\207\200\241ua\224\021*F\354\272\334\375\350\245B\310\001\226~\200P\214\330\226\242X\\\305;=`\315Jf\040{\036\361\033\264N\026\235,\316\343tj\0072\344P\370v\302\367\236}\347\361B\263\015jG\273\004\236\034\245}\002\355\221\240g)\253\023\367\245X\347\016\314'\322\035XXQ\337\031\316\035\263\247j1K\235@u\01245\264wY?\376F\217~\230\347C3\370\353,\233\306\334\343\345\002\317\351\243\274\027\215Y\211\312\261\260%l\201\021\376\263O\020\214o\370\374\363\235\353+\017\222\020\343\011\230\223=\333\274XV\035\300\350\331\271\221\261X\373\214>\272\342Lq\327\024G{4\271\362}bv\277$\275\252\3205\034\257>4\326@\203\242\027\273\204\265C\020O\025\326\040\326bg\207\237\222\362\335'\373m\327D\201D\006\225\225\257\261\334\344\265\304\244\027N\310j\210s\347A\247\023F\350'Z\005\246\371j\243h\217z\357$w\277\334\000\274\253\242\213\234[\021\211\324\307\242\211T0\254\352\0241\031\262\0300\275\300\014\364F\234\357$\276Ar\0139e\342\3774\014\242\033O2\214T\213\344X\012O\324\203\305\366\325S\341\235\242\257G)@\330\31346\253\234\250$4\341&9\332\355'\331P\007j\036\202l\356\321\327\370-\337j\026\374\011\306D\236\276\214z_.2o\245\250\337\347\247\2649\216<\036c\250\35084b\314\375\014\274\020\037q\366\262\321(\303\270:1\307\004e`f\261\332\370\206a\236\373\200\310\347@F\2420wn\262\002\214Cw\305\324\032}n\313\024o\245E\027\024\353\015\211\211:\262&\247\352nsA\214\256,X\212b\243\342\204\337X\210Z\270l\027\231\250\310\226\257ae$\223<\007\243n\364v\215\257\012\303q~\327\331\236\352K&n.v\027\264\335F\373S\374*\333\247\236+\211\307\024X\012\033+|\177\233\000\036\372\350\344*^\371\016\223/\364&$\303\010_\323X:\352\"-1\323:j\374\256\374Xe\326\034)\350\300L0\023\216~3\003\377\234\237\276=\352~<~u\361\2634\330\373\253\004/V\352\276\323=\247(\206\272\340\304\304\334W\3214R\350\2026\335\020\230\257\210-o\234\213\305\367\350\252\354\363\231\264\350\227\200\200.\220\270\362S|\334\"\353\031\014\340:\273)\302{\227\215\312\014\350\355\251\226f\364\330\327\017\344\346\347x8.\371\\D\331\366T\302\347\345\264\036\007y)\226.\217\373\370xg\340\232\273\036\377\033*,\267\025\305\313\021`\332\020\003}q\231\355\301\270##\363\240\314h\364^Q\244\231\3520\3148\216\326\366F\025|\031y\231\365\260\262b\212L\256p\313;;\300\237]1\227\3050%\010G\040k\250#KwvX\346\006\305\020,\242\263\357\354Pn\006\214\326\316\025Z\345\023\317\323\240\274u\251\214\244\016\000\214Jv\266\005\013\277Jduc\351\313\343\222Ce\265\202\231h\241\000S\026p\034\200(\2371Y\202\212&-\017\002\"+N\001Yzv\204}\273\036\245F\360\356G^\012\021\2067\2531\272\032\017\266\213(\3665\214\004\271\032\377\335\365\350Z&\207\020O\364x\201\347\265\265\314J\350\270\021\022\251\372\224\263E\344\366\301\277]7M.p\013%\366}P`\005\251\370\336L\313\363\313cY\262\016\237\262z\321U\031"_buf, - "\030y\316U<\345\207Q\277/\010\325\006\350\236\350\231B]\276\016\346Ox`[`\037\234E\240\000e\353V\276d\017v\367e\231\036\034=h\007\265\003-\030F\232\223\260\264[\350\215]IL\245\353\002\242.<\350\242\234\201BOBW\243\226\266\032\276:E\201z\015\227P$&\307\302\231b\237i]<\375\342\021\252r<\016\343~\301\312\313\020(\363p\343/\017@\330E>\334\251u\3361R(\227\314\240\2772X\336\252\307\351\273\2507\311JGH\227;\363A\237\267\211w\216F\330\033\024\367\250D\017L\244\277\027<\003E\206\013\23049\207\214\372\3749\306\030\241\320X\016\342\243\256F\024\000g\267\214\007\220\245Q\374\230\233\373\331<\316\357ZZ\307[4\237&#L\302A\016\2348\204s\274pC\243c\036\210\217\371\202\213k\352/P\213\220q\005\247t\004\202\024\002T\307\364\0168\342}\0316]i6\240\040\351\273\356o\040\374\242'\253\375\202K\326\230axGW\015~\356\367\344q\317\256\006\277\242-E\253X2\227\325\216VS\306\251\022S\332+*z\302\265(\317?\276\261\2316\325I5\325\3617]\2035\037\376\334\233\036\021\206\001Z\256V\235\000\224\032\215\230\221\220f\323a\002C#v\215\241)\243\"\2457T=e\237\211\257b0\020\370\3660R\342T\302\341\035j\275\231\261o@\010\343\222W7\217)\302\242\341e\2075\000\275\360\365+F\040\242\210\012\\\321\032e\024D)J\203\265\000C9\345\301\361_\317@\030\203\352\301_\333N(\040\266\365\013\030\004\245\335\332@\317\357Z\040\256\223\253k\023\206\033\004\210\341_\343\340\257kn\020\306d\274s\221P,0\323l\032\015\253)T\301\334\263\002\001\317\224\211\213\373\011\210\247\323\363i\037\230L\351\225\014\203y0\034j\311\214\015eF=3\264X\353\312\340P\320\306\3436\330\341\351\344\213\260\352B\016\307\317E\012\364\375\352\024\363\022\020\240\260h\345\316\040?\313\351\236\305\021\203\300\227[\336\270\012w\245\226W\006\340Lw\357\356\324\310DnW\272\267\343\361\236\315R#\034\257R\032z\243\321\252Xp\233\344\365\\\311o&\331llt$\313\302y\243\336\252\2366\335+\006\246\037\337\326\255\234\037\212\040A\265\257\025X\363\242\253\222\357\014z\356\304\270$v$\311\334\245\017\323\207\320\322\331\004N\"\275\274^\220^.\342#\252\213\040\275(M\360\017%\341\323.\3608\313\213\310\276\354g\303\245\240\233#gS\344-\033:X\307-A\005\234\247\026\214Zp\236*\261R\003\377\340JT\335\032`\344\330l0\362F\303\230^\303o\342\010\2145v[\344\2655U\327R1'\261\305bk\346$Gn\2777\211Q-\016\213\337Z\270\346\274(\363S\017\333\0116\361\226\020\034\327%\272}.\356\037\247\347\314\230\347oB\314\016\204&\303\360\255\262(}F\332\264k\023h\011\214\332\304\251\2662h\312=\250\357\200\241Q\245\347\221\326\357\332\244T\242T\327\\Yk\325\362Ik[\254\251e\007\266\226\3204f\232+\346=\244\350\011\227A\244Zy\350\016\247Lf\324r\022u2\270\371\202\246\347$\355\314\335\346h2\251\242i\021\203\327\031\226\232O\\\307Omj.\205R\233\236\265f\0061z\006\366]Pj\321\264>\265\206/\344\266\240\215R\206i\256\037[k\3577XS\203\310\305\022zi\233\244\040\027q\027\037BE|\022\004t%J\036\215\264m\222{\300h,JU\333\031\323\235\213VK\340\314E\255E;\007\241\271\006\367\235pjS\2542A\025\207\005\256%\216+\011\266\036\341\201\214\357\";Q\034\352\252\200\\\352\311,\375\301d\347\031\2166\346\271h\307\003c.\272\021\255\034\253m\017\352;`\324\246\0279)\007\306h\301\370B=\224Z\346\263\210+\264%\255O\206\272\347W\261\270\274\040L\244}a\352\325/\242Y\316']\322P\314\224>\037\200\330\372.\314\325\303\254\207\367\313\331p\246w\246~?\314\314;\265\302\206\315n\327tc\274\211\003s\207\351\310a\327\034N\371^\202\265\347'a\354\354X\326\361N0\212\243\264n]\314B\336\217&\375W\361\327\304\300\270\322\320m/\346vC\033\257\374\303\337\242I\022\245=\365\026\263\236\371_\356\015\035\211\325\027\000.\364\255J\374\255\032\017\"yi\013\355+\223\257q\310\013xz\263\206\343\371\344t\022\2459\232\340e]\231\253J\024P\276*n\371\273\214z_@\313F\3416\236\210&\253\360\365\323\347P\336\251`6Da(\222c\016\373\030\350\247\341\264&\331\026\345\304\311\215i\010\230AT\351\331\252\204\264\342jjP\205]E,\276\367\213X\375\246/\262\332\275\330T\363\030\231m\357u\272sd\326I\347\023?\253\226\307\247\020\335\303\020M\"s\002\333\333\224<\202\262@\217'\331W\214:\206\2572\006\031z\2121\211\217\336\325\361\200\267;*4\376\311\270lU\002\376\030o>\264\372\361tO\372\366t\350\346\225eq\214\373\2428A\217\030;\251\201\313RK^\206\362g\350~!\250eEH3z/\013c\026\002\263'\202\007{qX\002\211y\302\026\217_)3E\036\332\260Jr4L\345a\307\362\221\270\244\222\251R\324p?\255\224\260\204\240\305\241Y\302\254\"\313\226MmZh\031\312\270\\O\222+G\225K%\234Cr\030\\\024\035\\\216k\376#\332\352ZzH\001\211\214#o`\025Z\236r\000\305\350\3653\256>\204#\364\\\010M\266\337\231\003\002f\332\212\373\376\031\314\313k\254\236\"\305\212\307g\353t\201\211\264B\223\216X\316A\301\321\331\255B\002#\353\241\233\000>7F.\303\335\220\362\340r6\200\315\313\257M0\225Mo\030ct\221\035O\336\036\3215G\247\333\344\031\351\245\245\224\316I\217\303s\331\251r\265\254j\377\341\026\341\260\234&\202\251VXk7+\340l"_buf, - "\025n\252\227V\001\0046\242\2003e\373\251ZV\216\265/\311\230\205|\257\303\027T\332\340oA\002JF\033$\342X\342.\034\354\040\352\273;\035D\040\333\037M&\331\344(\245\324\006\300\365\373\241\342\234#\356n\334\264\223\344\357\240\343\304r&+\002;\330\007\252\022\365\306\366uT\352u\214\027\372\242\374u\204\002\233\373\201\276Q\307\225\253\312=\240\036\224M\343\320\274\015vFo\250~\211o\036\334\216\204X\012\206\214A\273\243\002\231\265|\330q\304/`\335\360v\357\242\261\200>\212\306\252\237~\3235\022+P\007\246\316\300,Zf\220\016w\333\222%r\305P\250\267F\336\260E\366\365z\235\205S\020\243<\016\241\302\304\024\177\314\266\0056\212\246\262\314\271\356\017\210\225\240\304\001t:8\024\205=\333\225\2427\030f\321\324\250\324O2\255\2041\367r\217\012\356\032\320\361y\022\361\007\347(\024\304\016\257B\343\316\352)w[mH'?\330zx\272\345B+d\256K\040\301\342#\346\377\331Z\033\004!+\222\2112\003\246T\216g\323\306\202\303O\361\265\000\040\237\017\230\367\014A\321\373\023\236\373\225NR\201pr\254_0\374\020\3247\015\241;\314K\323\333\221f\375\215'\2242=\026\217U*\037\320\364dr\345\206\327\327\341\025@\375\032\367u\227\007k;\275d\3311\235:\221\352\241i\267\362\261\310\256\330j\262\371\016=\357\356q\327\027\341@\2569\244\030\032&\367g\221u\245\177K\001\364\233\365z\274\320_@\025\257\345TB:\360\040\014\236\010\\Qr\030\267\306\324\242C1\224\023Y\356\250/'\032\216\314SLP<:;;=\013\203\305\342\305\3050\376\032\017).L.zbY\002)=\037\233\305\242\236M\315\362#\252\241\245y\302]\313\374\354\032\302\234\235=D\261t\367\366\315~+\253k\364\362\317\337\335\264\246\344\237w\351\340^\335\323\245.\026\210\271_0=jj\353\234~\040\246\322\351\262\204+Jg\303\267P<\202\203\332|_k\351\304\237GW-\271w\251\032@\321t_iU\336\271\246\335\226^\257*\275\373\347/[\357\027\015\275nQ\246J\\\341\202\340\235\376\250[@\352}ia@\332.\032\346B\255\265\037\017\232\356\343R}\236/\005]M\375Yz^{0\331\230\215\245\341E\203\256/9\325%\034\200\2562\225\214\300\261\034-\313\301\354\336C\207\305\040l%K\214B\371R=\014I\224\265\306\040\0253\227^&\372\227\345\337\213\204z\303toq\327t\014\262\022\312\241c[y\307\215\252a\206qH\372Y\020\245w\024u\200r\034\260S\007N\037\316S[f\263c\3644Oe\236\327\341\035\232\021x/\3758\305\204\270((\365\331\001\312\214\237-\327\3405\257\321\236\3421\352\363fU\330\273\360\304\334S0\325q`o\327n!W\240c\255\311\256\033\276\300i\307\265\332\236+\027\205\307t\214\275\272[zv;\217\341\373\371d\272\303\331h\006\325\340C\035\241\316\274QQ\234c\233\201\374\373\360:\031\366O\262~|a\275\024\301R\203\274\212\240\262\024\256\312\366\221\335a\246\252P\361\232\275w\007\371%\010\272I\267P9\345\250\334\201!\013\035Y\031~G\337r\027l$z\241\002\267\207\177\302\3428cZ\352\276oe\210P\2528y\177\027e\253\034\021C\177\204\342\267\017-*<\3675\204TN\264\014\357\352P\325\344n\225\257\014h<-\345\244k\311\254\345\000\227\345\202\363\327\361=:p\017\322\\=e\320\235\371F-]\256\367\303%\326\302\034\211\236\366C]\031\232\315\256\217\352D\274\202\012\302S\207\336q\001\323|\326UH\272\261\265\343\040P9\204\236\372K\257\250\200w\271\327U\371$U\371%9\242\343\276\274S\005,}e\264o\316\273\0115\323\040\375\035\312\"c+\030\260\364n+\240\265\370O\033\252F\214\260\263j\321b\012?*I1\014\261\332r\307\336$,\333\351\276\030#\375.\315\243\344\205\244nI\001M\2245\032e\233\220D\211\342\201\202\013\233\3167\325\316\330\312\216\225\340\243qS\016\333\011\342X=\021\0215\373\361\236&\2466U.j\231\037\2450i\266/\344\310\246\326\207\023\002\010\016f{!\0076\365.T\033\246\373\304\375\377\006\221?\316\040\"\315\005\236\245\250\260\027\374\2677\243\324\276\376\326-\031\025V\004\373\306\273\312\020R}\315]\006a\376\333m\317ji\007}\021\206\221\273\366\251\017\022\270s:\272\342\2544\331\366\010\\\036=\276#\002Y\265\275\237\014\365\235\256\347Bw\256m\332~@VY&b$9v\022\337\341J\035y\223\363%\356\262\261i\343\012\315Y\273\222\326\245\214\023j\200\313\016\221\030\221\343H`Y\364K1tA\205\342!\377\345&-6\346\207\232\356\231\367\234\260\021u\032h\231\323\266x\204\306IsE\225\257\307J\225\026E_\352v6h\322\3515\012Z^\\ek\367\333\270U\373\366\343\320\274\206\223\264>2\260*\263X\040sP\206\265\373\343\015\355\232\221\335\217\006<\365|\034]\323dB\035\340\256\013\233\306D\230o2\245Xv\040f\271#\354^\012uhr\200\305pe\206\023\245\205\301i-)\245\260n\273G\2543\361\012\036\217\0121\327b4t\264\\F\015g\343\243\311\304\325X\267x<\340FC\277\315x\370z\027\272gh\200,]\301\374&\032\207\312\0029\227\216\200\225\254\335\274\267(\352\015\312\303g\314\325q6_\011\256\356l\331\234\234\323\005X\025\204\252\316\260P*=\373\336\347\371\24764=\374\346\271\274\371\276{\023\327\015\205a^\304\334!\016\373\240e\332\254c\346Tn\035P\312\027\315$\301-\032T\033\355\215\237\374I\005>\3036\370\266\242\030)H\277]\242\344\022\015\327+\234Q<\3023\202\340\2026\336,IX\260\3346\005\246\212\301,\267?\353\336{\305\226\026\301\225\022\221\327\347\336z\365x\364\025\324#\341\323\307\257\330\234\336*{\316\332\035\343\265\244U"_buf, - "\241\322~\270\373}6\245\335\357T|v\347\327z\314\306s\270=:2$\333\251\2171\236:\374\2131w\035\211}\215\370N<\034\275\221/\352\320\276\305\303l:,\177\257\236\202\362:\231\032\257\222\316\342\276^\360f\022\307\206B\365r83Z\035\336\231o\247\376\021\343\273\040\013\324\2351\202\227\223\344\352\232\274Ho\333+\316og\024\267\207\327\373\315\036\037\373B\243T\3539\206\375\226W\2743\352\335\271\000\022j\324\212\016\\\261ol\246jU1wS\025{yG\341\266S\012Z\247~\303p\355<>\276\034\244\221*\231e[\220\275\270\320\313t.rx\227\365,t\261J<\244\266\254\346\300\026\207\306\375Q\3611\311\214\202\301\361\221\030\263\253\350\265\350\217\367\244\007x\202FI\032\015\225\020\261\373\016\222:s\207\231\265pb*W\031>\340\273\273\210o\247~\374\376\034G}\346\370M\353\354\274\344\201\025\374\040\022E\262A\010\205\032s\336\234\035\034\037\007W3P\314\225\250\040\024:\233mH\276\251\017\015[OQ\007\377}Z\334\370\245Y|\333\213\307S\263\252\026\273\253F\253\337E\000o{&\332\213=\232\004\260\211\345\374:\233\342A\230^\305\271\311\235\211\313\316\220\261\273\346T\222\037\014\223\320}\365G\340Re>y\221\276\267\027\204\346\267\274)\230\2360\266\317\025\346Y\347\256\276\014\366\307\307\234\215\005#\241\317\331\321\233\343\363\213\2433\231RG\220\254`\201\024;\274\021\374\347Bq\311}|\002\365N\016\336v\317/\016\316.\272\037\017\316N\216O\336\234w\317?\274\177\177vt~~|z\"\247\355n\307+v\337\274=}y\360\366\\\202\010\214v\222\333~c\354vg\307\332\264{\352@;\352\263\"\020#\272\262\332\353l\362\323Oj\325P\274\371\013\356}\223;}\357\234\233\027\205o\361\237\023D\341\220\257\330\203\2217?\332|\010\263(sO\035\\\005\302\324\252\017@\324Ot\211j>+\346\244XM\2116!6*Q\257\216\030\211\335\375\262Y\016\301\255\225\031\247>\336D\221`\017\342\316\204\335\016\241\202\014rV2\011n\262\311\227\040\033\014\340\\\301\310\346J\232\253\334+U\3405\025\354\345:Z\035U\224\036\256>\235\334\250\327Q\3718\363\007r\266\333\331q\026+\342\341\357\006\344\260\341J2T+\250\303C\336\263P(\005g\227\017T\362\037p\267\331\365]n\356\316\373&\242\353\2760\332]\230\323\034\335u\333\243w\027\036l\206(\010\322O\262$1\263\264'Ex\205\356\273\363\303\356\337\216\316\0322\275(O\006\030\242=\332.\355'9\036\201;\353+\233\355\006%\023\3170\222\3100\030F\227\300,\0028\357\202\243\277\277\177{|x|\361\366\037\3015e\256\305\210EA~\223\340\004\312}\024\030@\012L>\015\240\217Ut/\264:\340P\253aA=\341_\025$\271\010\314\330\300\277A\020\273D\030\376\234\242\034\306\353\033\204\321\033\346\332K\305\331h\004\212(\350/\263\021\267\224\362\364\337\010\365\375\204\330\261\225\201\024\027\240\006+\320\352u\314HS\252\240\245tFfb\273s\246lj\020\3032\261\257\241i\233z\273\2063G\321\277\206s\324~\014\027M\256\376\213\361\237\371\337\206\355>\032\037{`\344\025O\304\025\2176\346\035\270\035s\305\031r%)\233\274\021u\305\027t%7\247\356\006\302\003\2578&\022\243\225L\005\360\240\030,5\257\350\273\236;\372\335\007\334\377v}\027\300\273\217u\212=\316\203\323]O\3722\202\212i\247\210\321\331\354E~\372\230L\257\263\331\364p\230!\323\227\204@L\323J\312.[\211\010\361\336\012Etw;\215}R\274\307:H\205\213\032\263G\332\003\245\3528\272\270\317\253\370\202\305;;:\035\307iu;\325N\010\324\311+]S\302\365\035<\226\007\311$\237\322\315\022sl\312\343)\306\354\211)\345\007\017\011\206\336K\212\231\033E\214\331e\036\3031\224\262\246\271c|ll,BK\350\016\025\314\302y\250\201\346y\317\314\017\304\205]\212\361\030\352\2418Y\340G'\222\3709}\226\335\270\206@\"ES\177+\247\036\354JD\203a\256\017t\222\335\224\215\360U\202\301l&s\017T\264\363\222\226\314\030Za\242\275&\3543Y\240?O6\275\232\302a6n\270s\316;\004N\237H\372\013\2102\244C\031\025nG\303\233I\302\"\21182\314{D5\272/cB\330\337GC\036U\003\027\273\220\237\304\325\331\355\312Jqep,I\356v\245]\024\237\3047\264)\250|\265\251\031\364t\370\322\310\375[\020\352_\206\327@5z\321\344Z\254\277\007\310\323\332@\024\251\023>c\\$i\0363s\340\021n\000\000e\373\376\206\177\341\025\012\275)l\342\257\203)\354\213\313\031F\351RoI$Xw\224\274\351\244)\201\016\370\277\373\032\360\300\332#1\301\273\310\354\333\200\300\316m;\230$HX\363\335)\024\270\340\343\274\225\005\215\322\2442\"M\"2\246b\373\211\011\216\272|\212\256L\204\320\347G\242X_\032iV\355\274\007\203\357\037\261\260W\012]\232\313E\227]j\335\260\350a)`{\303\244\211\301HO\374\341\200\241\375,\275h\262\032k7U\325p4@\277\353\303(\035\344S6;$\040'\315M\341\203k\346\373z\031\246`g\333\3677\363\003\337\356\015O\0067\313F_6L\271mJ\202=\311\347\306\221\250\354\364\227\037u\331\262.wL\310\014\214\322z\327g\0017\022\362\030w8\316\307q\"\005\250B^b$\200\323t6\034\216\325\315`\363-\330\026\243\251\373!\235\204\351\330\353\373\302\272\331\003\011M7\207\377^\264k\270\241\025\273\255$\317\211\254\244\221niSW[:\227\344\016\362\256\362\367S\244\226\243B\343\023\271\276\213\377\2401(8\000\036,z\377a}\324\336O\216\017\332\336\370.\340L\251w\202+e\020\017\351\253\006c8\213gdmcB4\317\262<\311\215=\016\005x\026J0NGy7W\201\246\370X\323\274\217\362\316\353\207\363\345\262\005<\314FD\203\177P\367$\256P\307\347\323\273a\234_\307\230\177w\340\234\376l2,\245\275\227\303(\375\202\376\212\241C\040\312g\223\370\"\272b\232\240-\330\033\026\253\361xxW\014\334\336\220\356)\274\212A\002\021\331\351-\203\034C\312\361\340$F\317\236\210\005;45\212itu\234\243\322\351I\1777\352\246q\334\317\013\271\331\250\344\215\032G\240=iI0c..\214\317\251x\324\315\024\317\321{C\027Q\025\011\317=\015\023\314\376\027j\"\212e\226\337\256\273\237\357\356i\265;\036\221[\253\024\226\372N\252\310"_buf, - "\376]o\367\007\337\326<_B\253\253\032bhw\376\370a\023\333\2223w\0000G\374\257\335\207D\362\232*e\337\345+\032\3245\342>\374\301\215\027M\017y\315Qv'\245\275\003\3605\240\215C\220C=\230\201\272BX\040\243H\3463\361&U\364&\253B\037\371\252f\216\0205\312\3035\367P\2130'ae\363\222\326\036B\314\371\253\023\373\260AMW\311\212\002+1)\026eR\226\367\011\216\220s\254X\226\377\311\256\243\345#\231\245\"\336\351\021)\201<\210\314\212uJd_.24\253\317c\224r[\230\204\375\310g_\002t\270o\3505\305\335f\373\356{9\245\252\217\347+U\346\340\370j\253\307\340\367\356@\317\272\020c\333Z\224\235\226\315&=\342\230!\373\363\255\010\344\"(PVh\350\356T,C\274\003y\017\274\001\374\256c\310\216\341\370/>\207\312\301\324\270\374\363\334\375\375\013\2564\037\315\243\242\276C\305\277\371\231:\307\263\303\377\"\327\254\337y\303\372\035w\253\337s\253jY\271\3301X<\3423\316\302\342\360\034u\265\343\023\3174\371\234\355\025\034h\327\017\212\217n\036F\332G\320Z\231\323^\021E\035\203\247\363k\225\371\210\3048\004%\226\001\255\335h8d\336\357\350\010\207\231\026n\357\002vA\304\034m\370\025KN~6<\352\201\012\200W&\317\271\010/\004\021\020\254U/\311c\321\330\331n\020GS\320n\363V\360\217l&2}\214\351\021\040\205L\300KI\230\354\327\244\217'\226\034F\222\312\\M\350\262\327Z\360\316\216M\213\036\344\210\"\253r\357z\222\245\331(&=\220c\341\003\360\257e@8\021]\361}\301n\214\311\322d\263C\374\225/\250\267R\254\265\032\201\236\324l\3575UQ*G\354\210{\251d\022\243>;\2067\040\025\312d[\373\201^{g\247oe\232\253\013\3715\306\341W\040\323\366d\223,\300\3561\221\277i\365;\216'I\326\357\224\347Fs\366\213\373\362=\206\005q\314f\012\337\272\0243\304\212\323\306\275\273\330:\231\303\315\247@\267w\354\333\\C\342.[\030\223C\267\310\311Q\356\261\026Jt\276\2602\350\036\237\015@\255vP/f8\030#\027\3024\303\225\253A_\251l\257-\002<\022\237r\020\335\202\361\301\371\312F\022\277\261+0\340\356(\371\247\330P\010\351\347\204%\030C\036\040?/x\357\223\211\303\212\034\021\323l\224\364:\344\260G\177v\247\327\040D\366\273\003|)\\\341\241\347\336OZ\257\3357'\037\016\273\335F\360\333oJ!(\000\300\032\273\2152\262PL\274IJf-:\302\276\304\361\270\313RO\204\027K\301\270a\205\014\031AET\261\207q\270\270\010\232\305N\260x\265\030\2161\370\343\"k\270\350|\273\357\353\245N\017\036\320\362\317\002S\257\342)*`:H\265k\271\200\335\313\010\016bRL\276\351\003\332U\211V'%\006\036\235\307\235k/\235\011D/0\201&z`\377\210\245\300@<\327Q\372%\017\376l\3119\246\304\252?>\276\364\345\347\375<\024{\217\20265\242\356}\015\254\232\342\016\033<=}\301\2473\"\031'\236\251;\341\302\367#\317\300\030\264\350\261(\201\330\017\355\225\327\002c\310a\321\036\2441\274\371\320*T'\203\036*{\205c\204\365\346^\223fP\177Q\214w\360L\336S\255V\205A\014\337\004\274\3131\264\307bT\024\007\003\262t\241\365d2\203\315\305\002h`@\355\273\305\335*\011\252|\261\212\245a\365wvj-\017\254\012>\205\354\222o\247\\\017\023\377\314\350tq\366\017\357\262\210N\315%v#\335\304\2530\351\321\377\036\274}kws\205\267\025\310\320\016\2431\032\222\302F\3134\037R\362rD\314A\017o4\345\345\223\025\232\261\230\321\331\207\223\213\343wG<\326\267\230\203\265\212~Y\367\373\025k\207\014\360\003\216k\301L\013\003\327a\226\"~\034\033M\317\361\212\362Jhd\224u\326\004\345.\311\257\253\252\376n\017AO\024R\353\244\360Yk\334S}\007\264=\304\361A\377\205GG-T\250((\002\017\260\"\362\246\327,9\216\360\302:j\024\020\254\314\011\303&V\325\306'\354M\3610\032\223\233\22006\211\255(!/\213q:\201Z\006,Q\271\272\246\350\301\273Z\025\034\314^\242\012\027\350*\226'1=\212#t\242\3228\231,\364\035\000\240f`\230]T=\2608\204\2316:\3105L\264\241M\022\370vn\343\374\213UW\231\234d,6\315=\015\350W\223@\177\261\003\250\357Pr\344\360)\325\262?7\203/\341\027=\013\221\323\271\367\341\370\023\307#\336\317w\261\261\323e\231\267\372\364\024Z|&\374)\3448K\211\2549$\214\323K\200\302F\371\351\372\335c\226=\271\206,\226\304a\036q\304?\207\221P^\223\311\324\365\031\2252\234t\220\020\357\203\177\366\220\040\236=K\032.u\230F\230x\273\021\314b\367\341c60\351'\277%\352s\327\"\362/VZ\251\357:\343\214k\027C\015\216S8\034\262\024=:\245\012|T\224\321#O\364k\304\370j\217v\227\"\270\251e\351Wz>\312\001\317\330L'\037y%\002\204g\344\0358\305\040\340\200]\364(I\006\230\355>a)\177\2614\257\326\030\005\350U\235\314\345C\027\307\340\366\2126\325\367\016\304\013\201\372\304\210\314\023\366\336\225M\254\366%\211\272h\337\034\0126]\"t\271\356\177\310\256b\264ps\216\331i\227P\374\374\351pH\2238\317\2063;\373\303\274pzY>}T\2027)\332$\370\333\270G\303\356\002^\323\342\342\344H\224\007X\276\340\277\353\354\016\270,_"_buf, - "\264}5\033]rV\315\277\321\326&_}F\205\310\245\220<(\252\312\224\002\313\223<\2027\272\372=\246\235\\\273\312\302d\247\341\376\327\330\234\030\235\275\212{\321\235j\277!\274\364\261T\261\336TAV\2564?8%\314\004\023\235c\253\276\343\330\326\354\227\257x\327M6\264\275\017@z\337l\205\375y\3601\016\320u\232\364\303\030\216\016z\364\241-\352%\000\000\205-\210a\345\356\202q\022\367\350\276\235\036\265\321\015\376\210\302UF\375~\261\270f/I\032D\342\324\244\315\212i\2721\220\"\271mc\347\327\321\327X%\026\340?\304\203\341\320\240h\272Q\017x9\306\310\351\345\230\322\200:\276\211\255\2160#g\324\243\374KD~\311T\344\341TzoY\315\2165\302\315q~:\022\3102\312OA^\207\206\200\211A\376\2041Bn\242\273\234\034\344\201\005\323}(&i\260\372\311G\270\007.g\003tB\320\3005)\270\010\032Mf\230\223d\004;\315j}\201i\2370|\0104'\205sx\027D\326\256s\216\035\330/\2467E\375\337\230\307\256\275X\260\244\021e3\3151\016&\014wJx\314\225\365\201\236\361c\313E\245r\237\011\375\337\330TNyQi/$c\217\241KS\001\261n\250J\365Ll\345\247\222S/Ta\210\256\226\220/\343\013\211\272\015\177\027-\375\252\244C\235\254+d*\310\030\221&)u\310\012\334PmU\025\331!a\270\310\370B\246\0203\325\237\325Zxr)\040\230b\343P\352dC\322oLD*\352'\027\01008_Q?\244w\203n\035T\212\333\345\213\\\022\235O\232\315\020\200P{\014\325K\201\313T0'\224\373yGgk\004>AI\312\3640\0306\214\307\035\202\252HU\215\201I\314B\275\3015\367\215\305Y\214\244\003\215\234\373\300G\340\375\254\013\\\217\322.\177\323\223/\252n-\367N\020\325\3073\205W6\371Q\310\350\357\002\251\225\350;\354\331\264\3552\012\270\241\271\223\220!\330\202\326\213Yv\276\321l\334\233\311c\025wv\340\275\356-\244\004f\227\266F,n0\311\006\040\2561\201\345u\234\375\330\023.\370K\215\231\273D\"\334\371\254\333\216\317\\\353\306\217=\026\253\004F\206'\263\177h\0053\304z\255\301\303\273\222\256\266\245\375\261^\226;\2341\326\354M}9\354\235\243g'\017\264$_|\226\245\227=\216\327\333\236m>\337@5\014\371F\313s\337\230xz\244\001\233,\305{\216|\013\260syR\330gR\211\334\242\207F\023\347K'\030\314i\257\374^5\320\243\257\331\217T\342\250\260|\260\237\040\303^e\023\020\025G\377\2167\375\226\210\304O\0176v\347u\266\271\360\314\336Yz\004\226\231\325\312\017\356\332g\266]\342<4\253\357!,\214\010TTq\\^\321\177\365\246Yo\334\354\271\304\212\363C\350Z\241X\223\226g)\006G\351F\323\356\020\304\226\202\252\317f\250sJ%\010\227\026T\320$MF\263Q\020\215HCD\267\361d\024\333v\017.\001I`\357\330o\273\"*R\351\225\254wA?\365\0154\003\355\314k\331xl\343\242bB0}AD\276\026\032\242\317\300\310\257xv\035)\034\200&rK\311\241\3152\215'\246.Y\303\230\247\014\025\217\213\262\253\177\351\245\015\377\234\342\351\306\376\334s\335R5\347\273\215\356)\367\366\217\340\265\254\320\303\277\216Y:\320[\345X!p\273\307\333j\016\015\256\373\216j\357\011\362\315c{e\277\304\375\\T\005\300\030\366\305w\253\257\\\014\325uf\241\001\340\322\324\351\036\003\261L#\250\212\015\3705\346\256\237-Rmc@\322[\020\025\311I\243\031\264\377h6\251\262\255R\233\351\367\363\240G\241\312\0324\210\032\212\240\277.\012\206\012\015\012\346\223\327\27224\334F8L\3165\370\025\031\002+\315\026\360h\363R\304\320\332\363\253u\275hz!\210\033t*\361o=\306\342+\366\237z\303,\314)\204\262\206C9W.\200\320\250\040\235\007\232\212n\302\272mp0\366v\371n\334\263\223\303\024\022\272\021\017\332\253;]\262\007.Y\332O\360\234\210\206{\245\266\"\251D+\245\252F\275\273\340\022X\345\245l7\272\211\356\272\024\311U\030\357c\341?\344X^&\266\242%|)\270\001MI\232FD\340\263*;\030_\216\305\036\275\203CC3\247\272\302W\254\211v\337\313\030\215\2757Q^\0145\300\241.\316\353\016\370h[\245d\365\224z\235\216U1tz\261\\g7]P1\257\330\376\312\343\270O\355\375\2620\317\271\316\322:a}\033\0237\327\3110\006\325\201*\355\005a\033\203\"\255\2554|j\003\337u\\\026\333W\031\200\315\220\352\330,\235\275$\203\040d]\210\335\027t\366\345\374\313T\032\271w\365\346\352\326\345_\312wp\271\355\2225Y\332\017V\353\244M\237\302\244o\234\373\347\333\037\255\215\270t\216\342p\225\212t\347\017\360\035\020\027\270\357\207Qj\275\311Qe\363\356\030_\322D\310\325w=\262?\277\271\356w\355\027\234\352\011c[y$\007\361@\006\361h4\033\243p\034\333\352\003\377\250j\021\337\355\264\240beNw\005'\306\232\016\3444U\306\251\314\260i\317\350\201\007[\307~Y\302cX\271}\013\200.Y\232u\221\0327x\332\033\000\237S\274\023<-\343\364\253\0375\230\367\236f\004\012\364e2\365J\036\346\266\020\234\314\371n\224=bs1\350N\250\040\263\341\300f\241\341qK\006\210\365\274\355\267{'3\254F\035\366\345\260'R1\331''\300\362\000\231\205\013\3609\021F\3564TR\177W\230I\006V\265\313\357\3370\2752HS\350\350\000\342\023A\206\331\271a6\203Oh\340l\342\302|\366\360\351J\201\017\177\270\3311\302^\356\310nCM\256\303fM\3676\360\031e\231\252GUh\325\360DC0\205\320\007*\025L\245Ux\277\264lW&\365\330R\201\355\005\2565\333\331\371g<\311\320\236\256w\\V\267\324\244\254\202y^\207q\322N\366GDu\220\324\375\243\272\374\330>=\246\327\017\347Yj\040\201\300puS\334\"l\353\026\205{\222\306-\214\261A\036\030\3210\210@<\276\313\023t\335\310\206zP\002\345\370+\012\205\035.\032v\034Q\265\213\022\206v\275,\005ao\222\364:eF\265\036\314\363\272\302Q\250\007\333\260\037\017\364p\011i?\033\375\030\025\230\251\036\214p\304\205\014\347A\354\315|\307\340T<\224\331M\214\3764(\340\200@\037]\305\335_gQJo\255\311\202\315\304\326_\233.\200;;\002\177,\264~U\245a\224O\033\325\307\355\261\\\224j\317\303\036\373y\327\025\016\177\341\2611\244c\255w\337\313`}\314p6\215\357B\016\3005j\311\205~m\003\256\375(l7\203\365&A\003\256{\225\244\310j\351\027\354+'+g@\327J\201\256=\010h\362+\332\273\000\3642\014\332SgH\341\212aN\313\240\\@\375\245`\315k\351\032f#\243n\273\265\341\253|\235\214X\347\317jU\316\215\3124\012\253\262\307\027\325\343\276\274\313\323?<\331\247\025\305k\026*\250\320\231Pc_\242\212\376\323c\012g\006\240\256\001\040\263\326\020\364\235\034\235\327bw\003J3W\264\032\025\255F\311\260_\331\246\203\350am"_buf, - "\256\201@\352v\205\315FJ3\177_X\205q\221\034\200\307i\035]\211\037A\331\274b\247{\247s\306\204'\366\374\273\231(\2109\344\261\205\006\372\364,\237\214\3458\022\3742\352\365X\274\310X\335\374\315`\245\345?w\261\371s\326\343\274\323\377p\206f\001\013\033J\021\367\35651\304\271<\010\212\364G\210\200\236\006\023ad\020\345\271\202/\027\376\232\005\374\247B\343\300D?\036\254\246\225\030\0257\323\350J\211\201\236\272}LM\210a\230)t\2130X\247\215N\200_\302\225&\000]\016\332N!\232\315\021\370\272\335\017\024J)YN\366a\2021\200j4U\214}\002M\213#\353\251\304\011\374\215\003\206\177\000\311\237\313\236\304\032\007\211\000\334\367\311\202\374\263\234\216\317\005\252\306T$0\230\020L\342\223\230\2056\362\342\015\025|\374D\253\000\037\032\237w\235\262\245B\347\022\027E?\305\361S\024\371\316\240{\337\362\344\331d\212\253P\000\303\037>0\202\323\314\346\336lG\305ZV\360\037Ny\277\000r\277\000-\307a\261K\224mR\261\267\276c\0271\365\206\342\040\011\306\224b0rv\\\355\372v\0123\215\345.\234\321\007\215\304\334g$7=b\270R\032\032\376\255\234\227\311\264\36458\312y\335\374&\032\207\311\224\357!/I\261\021a\336\305.\022qXP\027\2338\027\273\032\265|n8Q8\021`=p\243@\036\234\365\247\350\362=\354\366\372\203\220\227\3346*\262.\307\223A/\\\276\015\236s\352\375\025\250w\265\205\246\327\347\001\374[\3329\357\003A\340=G\321\247[2\347\243\223r\037/\036\317!BW\037%\362\375\210\340X\227Y6\305|\262c\321\037\205q\355c\254\234\356\020\204\215a\345\241\302\351QD\036\216\205M\313\271\215\374\373\204+\301\271\265_|\007\371\230\207T+hI\227\340\035\206\246W\261`\251,\372\210\210H\002\252\024J\0064\200\005\227\310\247\214n\037\316\257\302\236G\203h\032\377\230(t;\250\026l\007&Qp\037\205\343\224\317\207\243\001[\342\325l\312\014\375i\210\005\005w\245_>\366Z\210D\335\034\350n\202\007\"\376\350\315.]\366*vM\230\304\241\273A\303!W\371\006\243\346\235\037G\311$\\i\221\334\005\347\230\234\320gv\352\341g\031\007N\220m\376k\357R\206\347\276\245\310;\376\312~\211?\350s\3343\014.\007\267%&\250\376*:\343\203~\322/\253\204\312\034\324\364\326*|\237`\012-\246\244<\203\026MV\300\017\202g\010\350\336}\256\372\226\021\320\036c\354i\271\040\300\243\302M\030\011\303\014&\212S\327\015U2\327\243M\330T,:\"F\021a\206T\274v\224r@\013\363\304\225\221\323x\222]v\213\000\213(*w\223A\321\336\022#\004I|\242\375\363Ye\316\002]\267\240=\2610\211\350\334\014\363bu\032\251s\237\367]\373\234\013\252|\343\345\316}.\206\216Y\372\036c\223\363\231\\&\021\3625\223\303\263\336\374\210\374g\333\321(l\267\200N\315\336\331y\344\265[\300~\304\005\371\224\252\310]\356\320bW\207\313\031N`\015\373\241r|B\353\245\040m\354\272H\224:D_\227O8\355&#K\331\357\245\262\250\204\226g\301%\256'\315\212Q\360\022TrB\026\040\020-\274\355?\333\376j\253\242\332rY\265\010\241E\341e\333\277\016\321*\253\262\3527\216H\237\350\3506Dd\207Q\033\010z\305o\363\220\015`+\260\006\253\215\022\365\310\244E\261u>\015\263\317\312\257\353\344\263\2332\253\305\024nQ\353~\215&\011\276\213\013-Q\201=\265\265\212\311\356\371\225\251\242\226\254\311/\022\245\224\321\225\246]\233\354\234\035\356VW\023\001u\2730\214\304q\263X2\311\335R\037\003\307\250\331\037\261\020\007\374B\023a\243\253(\232\217gP\375\261\327\317\205U\276\302\350\3562\246[\226s\333\006\317m\351\177\244[\230\377\246S\321\333\371\313k\341\310]n86#\210\324\274\342\373\002\034\253\375}7z\004\242\031h\227\222\025w\222b\222\344U\350\030#+/\006\351\033\040\305#\302#4,\224T6?~\242\303\360\330o~\236k\037\233~MR\237)\033\216\247\376\247\317\241\025\275%j\332\021]\324cF\225d8.\3020\002N{\331h\221t\302B\243\224\260]\027\346\234\261\263\210\271\333\027\364\2403\254\300\177\273\025-\370\035\243v\243\215\241]\023\036%9\004(\015/\020\356}\016Pz_D\227U=\242\277Q\345\350\314\340\004\342\346\017\377\254\034\362F\331\220\213\013[\023fw\230\214\3102\241G\275\026x\230\037$\024\012\2204\333\225\007\014\252bm\346\035\224\213@\036\300\317\012/\227\260\304\232\361c\335'\230\237\333\323\202Fxu\367\273\265\226\313\247\277\2767l\347\201\3011\364\013\353\256\301\363\2131ym\024x\243\366hx\254\332U\015\325Kb\016\334\272\336Q\250\022\244\373i\255\003a!Y\023&\212\272\306\355\010\036\326l\337\315:\3326\253\036\261\377\241$\200;2t\325T\004\002/5\330l\012e\371\242a\260T\311\214\232n\304W\261E\357\245/U\021\371\002>}\366\3132\325\036\353\237\276|.{\224\366\240\247l\354\335\034\213&\316o\251\257@\251\214\253\337P\250\377\205\370\364\256\301[\372\253\272\235B\357\033-\347#$_8\005\211\320\260\355\261\225\010\327\335*\036_\372.\345\221\271\212\213t\032\322\337\267\230\224O\3223\004G\217lK*Na\265\265\014Fl\360q2\014\225\255\362<\220\316\372.2\326\345\345\364\273\356\372\204\310\234\026\227}\305\324\221\243~\256\262}8\344F\205\"&-\365\314\300\211)\277\013\311r\036\257\256y\271\263.a\253\322w}.]\002\243\006\267\376>v\355a\323\302a]\211;\345\211\362O\221\216+\001.\241{\240/\353\267\260\375\221\227j)A,A\225Z7T\232O\277`\260LT\022\313\347c\341\312\371Q4\365J.\002\232\353i\221\342\036\221O\375\300\360\253\000S\000&\007J\227F\304\021\031\337Tc\375\2332\231&\033\305\275?\303\273\205\333\037\355g\357\360_4\034\024\271\261\247pm\244W\277\000\200\177\010\262Ty\337\342ppd>\237\302x$]\035\013\323\006\277\303\254\366p\264\355(<\307\260\323\040\363\303\037\0060W\342\003\267-O=B$\004av\337u_G\026\365l\243\237\243R\225\311\257*na-\323\340\367\276\023\320\221T\357\241\200\013w\253\022y\253>o\200|U\236\231\274\300\177Kc\332rx}y\000\210\002\365\236N?XE\237xe\3639\224\3570\372\212\315E\216=\3547v\347?\361\212\207@E_\356\330Ph\223\366\2000I\304]O\020E\351WI\025s\350/\217\340\204\355b\040\017\260\312\012\346\361\007\331\\\035\321\002\334\2367\236\015\322\021\374\265\362\271\211l\361\000\357\034<\362\237h\357#N21\224\260\321\230\303\351\313\311\331\214\375)\267\247\352\333P\346\373UlP\3155\321\277\033K6c_\010\243\036+gqk'\030\372>#\020\3455\326\316\216\220\037\314+\020\015\207\207\362\"\344\030\023J}\215\206\241\371\014\345L\334\211\250\314\306\315}\312\036\204\310\220\257ec\265\245\335\312\016\375=\336\340M\220\220\032\230\202m\335E\3055\336"_buf, - "=:\316\263r\235Y\324\013\343\026]\3775\232\245\325\215&\303\354\006#+\343\025\356|\015g\343\361\034\015\343\226r\011\226\360\305\3677\363=\345\334\255\334v\226H\261Z\272\363\3468\031\177\304\351\310\245\013\327\331\350=\032+\217\307\271\216HzA\254\222n(\317\023\022\362\347ne\237\254%0\312\217W\332[\002n\365Y\353\241\220{\346\372\376m~\322y\020\317^(\3337\001w\305\222\213\274\342Y\340\302z\346\017\026\345r\320\334S\3743\233U\02671\223\302\363R\216k\311mGT\005\253\340\2312\017\254?\317\363k\002\360|?H<\010\233\237\300K\250\314f\251\337\230\267\202\372\277+\255\225\373\271@(k\330\014\374?\312\341:U\221o%\015\000\\m\302\377C\025fU\035~\320\203<[e}\\\367\0003B\260A\\\262\\K\314\375\364)e\024w\204\274\306r%\236#U\263\343+\026\0020\011\033\301\353\017'\235\332\3356\261:\374\215\370r\015\301\012\257\333\303K\366y\007Vj\214\323\036\263\273\315qc\312\307\036\377\210\327\337\342\346Z\334\257\026\317x\015[T\260T\334T\250\027\335>3\332L\277\262E\277,\321M3(1\253\313W+\242v\243\243K\270\037\311\214\207\016\007a\243\341\2176\205I\331\025\243\333\243\031\370\305\3040\000T3p\307\320%\303||\323\225QE\374\226y\211\372%\032\261b\301\016\236\263\222\022c\275tJ\223\2355\2656\002\204\002tI\031\330\222\367yz\021\216\313\215\032'\241\225,S\3230\302~\177\360\204\",73\037:\322\270`\274\004\327\331\314\367\317\373\351\004\347\017\200\256\342\351\341l2\201\035t\310\342\326\207\215\026\226Q\265\320\353\323\311,\261\306\365\235j2WL\315s\245\037{O\333\035\230\024\343,\276\\c\316\354irt\370j\034\206W\344b\373\364\364s\265j\3049\215\030\371\022\261\030d\040\036\311\303'\215I\236{\234\0162J\270R\32271b\357Wz\376n\307\347\020*uECw\274\017\177(\361\301\225\0229\241\330\0215\033\250\252\265\267\211\227\305VO\311\021dA4\362\211,\316\3622\352;G\003:\022\037\256Z\251>^\\=\316Ke\2702rg\303>\375nz3\255'\206\255\244\000\374@\213\307K\005;\323\334s\320\222\347\3467\242\366f\241U\311\313OM\353S+\330\206RK'\313\233~5\315\247\263\227-\362Q\332\217\373!\015\330\205\322\032\371\023\345clo\222D|\312\346\317\200\210\342n\221\306\222\002=\345,M\310e\034\343\343\262q\206\311\343\232A6\275\216'7I\216/\356(HT\253\\c\341\265\2722\334\032?m\031\207W\212\033\325\211e\255\014\250y<\315A\377\033]\366#Lkr\031#\351\3671\011\011\034\231\301\022,\344R\300\"w\300\276\220\323{R'7\252}\025\357\213\000/\220\352\215\004_?\002|!\252\253\201\276y\314\342\236\353\364\304y\356\373\203E1\031\331!\252\270\243(\373\343}\033\270\217o1\367J2-nw.\263lX\346p\207\341\024\275\040\235\301\276\375\021\260\254\000\316\252&\201kdg{\362(t?\201\324\202/\"\217O.\216\316N\016\336\362-\366\346\350\242\333\356\036\234\275\301\210\247m\314\026s\265\332\0140\360)\376\331.k\266\352m\266\352\355\356\345\321\311\341\317\357\016\316\376R\304t?!m\210\351D\227\305\351\335\217o\033\377\271\3006{\3500\364\026\004\244A\372\206\200\356\203F\360\237\013\026\251\341w\264\336>e\376QFg\325c\356\036\274\372\333\301\311\341\321+\327\340\177\334`}\261\330\335\211\330S\246\013g\023y\351|X\224\321\035v?\226?\257\343!Pu\356\217\314\376G%\277j\322\256\202\372lh\316\230\354\247\227\277\304\275\3519\214;\272\322\037k\332[\221Gv\346\225\255`\234\3210\271JA\246\313\331\367=4\014g\203\360B\330u\351;\205\202\314\006\224h\213\270\232;\000'\231z\324\241\205\230\373\243\037M\243\320\235\223A\257\314\364c\255\354);zl\311\312}\030\202^\025>\305\376\032\301EH-[8/\230^FP\303\232\257\253\365q\351#\372\236!\025\366\012\367\340\352\215\356w\023\307\337$!w\321\207\3666\231\302Jy\322\316\332$\347O\242\3162\337\210=\023\032\001\251\353#\040000G\216\371\222\361\32369\030\016\263\233wQ:\213\206b\3070\267\241'\356\015Tv\250\273a\211\263[\2408\254;qcq[\277_\204\325S\364f\300\300\024j\003\012+\207i\302\242@aoj\224D1JJ\3106\003n>\031\336\341\376g\243\250\223\015\347\203\207\016L\022\013}h\024\270k\002(\206\273\245\200\336\222\026d\272\207\202A\307\235\\\031&z*\304M|\276\234\376\351\217\031\366\223\222q\2732-?5\027\270\302oN5@],uB\3657\216\024\212\330.\251\267\017.D\260\001s\024e\3317~\324X\304\341\202m\374\371XJ\263\335_\230\271\010\370\201\204\026CE\330\325X\337\036\346sDJ\332\235\017\264\240\011Zz\206\270\262.(\350yG\312\224V\262w]\316pK(\320m7\032\016\331\275\001|O\006\013\374_$\370'B\327;=y}\374\246{\374\356\375\333\356\351\311\333\177\24002\000Q\214\177\306rS\314\301\324\204L\276\341U\273\335\036\250\366W\335\356\302Oc\230\301(\012\350w\320O\242\2534\303\340\202\001\336\204\371\277\012\177\364\305\345\2177q\364e\371+\341)_\224\343\206\021\374%\216\307\310b@#\204\255\032\323M]|;\215'\230\216\235\353\215\040L\231\251<\000\357Hd$T\365\276`\346\340\2059\257FJR\221\026%\027\320\317!ts\201\275\260T\034\013\212\364\204\362\344A\332\177\233q\2278\365\362\304\247R\234g\263I/~\01321Y\323\206\274\255Bx\006\324P\003\306\367j\227\311\366\0060\361Q\000\015\224]7\230$HMt\326I\345p?4\247\300A\014\257\363f\340\3716\271\266\036\365pv\000\255Z\264Q\366\367\261\026\375m\355\361\247\004\275%\307\310\353\026\210\320\325wa\365b\367A\307\027l\275y!\333\205\242\020\215\300\374\206\"\277\216\220\227a\206'\361\265\243\001\222\215\224\231\230\363\035\021\226\225\022\016\301L7'@\371\220Y@\001)V\303\207\325C\250\324\2255U\201\227m\004\334]\277\316bX\322\334;z\253\363\210[r\\|\235/\240c\306\016\246+\023k\312\211\253f'\214\030q\007`\034C\024\015\211\010\223\\$}\321Ryb\373\363Y\257\027\347\371`6\034\336\005\"\273A?@\1774`\254JF]\003\240\332N\000\357\273\023\205\032-O\201\303\333\303@\216\003\375bRU\014\302/F\342\006r\035\345\207\327\311\260?1\040yks\360ze\253\266\300\361\323\000m\371\344\272\257W\204\201F$_\332xfb\3670\313c\336\312\213\\wS\304\266\350\317Y\001\017\244\203\374$\216\373\260\017\017R\322E\316\350\246\306=\031j\024\365\373\204\250P\333\270\234\\{\370%\360\364\251\326\037$\251\000S\217\362\203\262\211\000GL\0314\007z_\305\227\263\253\3470\264\230\016\001/\365\305\264\014rO\324\040\2737\354\311M6\3616\322\331\037\257\306o\266\304i\304.\023f"_buf, - "#\364\250G\223\266\351\274s\222M9\2555]\267\325\351\225^,w\316\341]O\211\340\244^?\250\0131\352N\262l*\371\262Ye)\300\3709d\256\025\034\327\361tD\216|\204\021\373\331\237\373\312\300\015\316k\365\362\224\211\007g\272a\222\226\026N=^\254\227S\003\232\242\325D\354\012\371uA\011b1,8\022\377\316\326l\3271(}\342V?\271\274\253\0245\024\264q\311F\234\341NBx\211\361xd\302a\343H\033O\262)\220d\334\337\321\251\204F\375\010t\"\213\005\333\323?#W\310\013\236\340\2411\225\033\351U^\353\254^%?v\354\213n\215\200\315\012mv\012|\353;\347)\022\345\364\326M\255\214\317\026\337d?\320\206\377\251|,\220Y\217t\215\001\341\012\326\345`Mk\0260\207\2462v6\362\300\"X\307Q+2\320\354\2325\313\317Po3\355\000\365\324r\235\220\376\252\346\361\350\311\231#%\027\211\374':\366]\002L\355c\250\030\236\223\367=\344\020\262g\3548\342\035\375Z\347\224\243N\371I\344]<\3571T6\014\233\243\012I\303\356G\225&<_\375\242\2041\010\313\260\305\232C\255\213\354=\307\337\256\353\343\3055\276EpqS\035_\005CU\271\254\241\333qf\243\250f\035\240\301A2\234jO\242T\325m\004\312j2\032\201ZRh\204&W0V\356\3073\206JR\231\203\223,\030/Iu\330OAL\375u\226L\200\3359G[w\003\231T7\235\334\235:\311\0216\370q\232L\223h\370\232-K\350_:\336\031_\277\300$\040\000u\002#\375\0368\317\237?\011X:\313\\~\216\350\226\035/\234\311\034\313O\374jB\343\275\\\305rD\226\014\242vws\235L\231Ac\231S\040a\225R\252^\307\274[\224Fh\271\234\264+rC\263\346\270Tz\227\270\237\214;R\323v\262\260\300/\220\214\362\235\235B\251\367\325\320\011\306_O\2478{L\3164\240.[\222ao\032\306\321\227n?\236\306\3148\3472\032\251v\241\267P\375\025\257\2550\016\265X\345Q\277\273>\020F\365\221\232\243\300{\310\361\030\251)\206EN\206Cta\340\006\256>\243\252\230\207c]\276\216\243~<\241\272dhs\247\017\351\325H\037\362S2\010\330\245n?\324\214\215G'\007/\337\036\025W\273\307'o\032\252U\016p\214\217:\2349=\274@?\234\037u\017\316\377qr\330\320\034\242\321\005\246#\355\210\305j0d\027\261\202\365P\301\312Z\320\035n\237\302|\002C\213R\272\333@|\241\277\024\210t\301\342\301\030\004\350[\012\372\300?\001<\000'\363\024/\"\212\337\274\377@\302,\011\302\301\233x\2247\203\277e\303\031l\257\325\005\343Q\354M3\030+L\352\006\304\304\345av\205A(W0Z*\206\201\244\277\237\301\337*;C\327\234\233`/\330l\255n`d#\323\374\206\220n\000\302Z\253M\025t\313\371\030\373Ykm\256\257\257\267WW6\327W\332[/\332\333\233\333\333\233\361\362j\333Q\267\335\332|\261\261\262\261\335^{\321~\261\272\322\336\334x\261\035/\267\267a\\c\030\342\215\335\244\335Z}\261\361b\375\305\312V{cu\003\272\200F[\320\344EY\223v{c\353\305\326\326\346\326\213\225\325\215\366\213\025\034O{\313\337\002\306\265\266\266\326\336jonBo\353\233\253+\333+\330d\323\337d\265\265\262\275\265\212]lml\303\2446\333\233k[\345\275l\266\240\312&\314|m}m\343\305\332\352\032\374\377\0064\331(\031\330zkec}\003\206\265\265\272\275\265\0018~\261\271\206\030[/i\363\242\325\336ho\257\255\267\267\2676\241\365\326*`n\265\274\315*\014mmce{\255\275\261\266\362bum\025z\303\331\254\226\342\014\207\324^\203I\254\255o\254m\254\255\256o\277\2006\355\2226\033\255\365\366\006\322\312\306\372\3526\340y\013z\334*o\003T\273\321^m\257\256nAO0\274UX\241xye\273\024i\355\366*Lh\373\305\312\332\372\346\366\213\265M\300DE\233\325\326\366\312\326\012\324\334\006\322\001\322\\\331Xy\201mJ\350l\275\265\272\266\216t\366\002\347\261\272\276\262\006\264\006M*\010m\023\010l\033:Xy\001\203\333z\261\271\216\323\331,o\003#\332X][\003\204A\375\315\027[m\240\201\225\022\272Yi\001\374\366\213\315\255\265\365\325\025\304\302\312\306V{m\243l9\251\311\326:\040\001\327\023w\302\346&\256oU\233\315\025\030\327\326\012\214i}m\245\275\276\275\262\261\266V60\300S{\003w\377\372\352\006\354~\3307\333ke\253\277\271\201\030\333\200\341\274Xk\257\000\025l8pu_$UAn\326\336lQ\3548';\243\330\3607\015bj\214\351\271\366\303j{m\013v\353\366j{k\013\231\311\312:\242|\333\256\272\335Z\331\332\330\204\375\277\015\253\364bcm\033\370\302v9\341\000\261\001b\001\327\233\260\203\200m\302\364\326W+(\247\335\202m\271\015\015V\001\317/\266a;lml\224w\323nm\000\276\200,\201?\301\250\200J\201~*h\015\370\015\360\346M`5\260o`\253\002o\333.o\002\033\007\266\301:,\374\013\330;\033\353\353\233+\353\353\345M\200s\254o\001\243Y_\177\001\314\034\020\374bku\255\234\240a`[k@4\253\333+\300\23376\266pw\267\313\333l\002\312\340\344\330h\303,\240A{\013\006\367\242\274\311j\013\320\325n\267W\326`\305\267\267^l\257moWl5\266q\32666V\326\220A\303\036}\261\276\265\276\265\275Y\261;\267\201\225\001\374\255m\040\263\265\027@^\300}*zio\002\037\200e\337\330\\Y\005n\275\016sZ+\357\005Xr{\035X\000l\263\225-8y_\254lnVt\262\206{~\005\352\002Uo\257\002\257n\227\221%\266\330X\003\\\001\222\341\014\200\325\331\\\333l\227\32214X\305\215\017\233\013\016\003\040\236\265\222\203i\255\005l\362\005\036\257\200\241\325\325\325\0258@7\274\373\277l\263op\226\340\336\216\355\225m\340\311\300\214`s\001\255\254\256\257\341\251\344\254\213\370_o\003\345\202\230\0033\201\263\014\017\361\225\362}\270\265\271\261\005[\012V\015\216\276\0270\247\212\343h\255\005\304\267\016\214\017\270\345*\260q\350\014\305\213\2626[\255M\020{Vp\361`\255a\177\040UUt\323n\255oo\002\231\240\200\001\274\033x3\034h\345\\\005\366{\033\310\033\317|\330\211m\350\354\305j\305A\271\274\331\332\332j\267\267A\246\000Q\016\017\262m\022\025\312\273\301\255\276\016;~\365\005\216\017\344\030\224\024J\017\327m`E\310\346@\264X\205\355\276\202'\331vy\233\365\026p:\340\303\033\040\314\255\242\364\003\273p\273\362<\006\206\212\\\253\015K\277\006\034le\003E\3222&\261\325\332\000\346\010<{e\015\2661H\246\355\366Z-\276\002\022\0174\001\334A;`\262[p\240oU\265\001\006\016\002\002pK\024\030\326@r\002\374\225n\3106\216j}s\035e\021\220Ma\231\312\020\366b}{\233d\375\365\025\224\314\327WW\035\322[\3413%\336r\300\367[\341\237\242\252R\366\263\206\360\261Bp+\374\200\275_,\215AA"_buf, - "\201\332\334\251<\370P\345K\212\312\274d\315\340\323H\306\024\210d\002\214KS\036\021Q\007\222\301\000\203\340\003\233\032\355\272l\320\021&\232\300JK\364\217\341\277D\211\026J\002\226\2509\002\210!\212\2114\344\202\334?\232\357\360\277C2\313GI@\227\364o\241j\310\332`\270\177@\376\227\340\2713\334\340\257\236\327\235\277\270^u\002`\177\016\003|h\210=/\007\277xBB\245\323\353n<\244\\\255\202\354Dv\222_\274Ihh\372\267\277\210\010\005\237~\371\354\016\256w\245\347\324\270\375\305\367\252\361\366\027\314\307\260$\342\251\352#\302\024\221\277P\014u\357\200d\007P\355\012\255\023\010p\031~7\036\226\253\311\377\300C\330j\204)\244\016|o\216'\177pCf\262\346\025\317NO/\272\027\037O\245\323\220\310G\345\317=\302B\3141\221\312\261~d\340\012\307A\007*\240\243\3238\330\333w\306\250\247\364(\240\003\255\004\277\375\006\265:H\270\245\236\226\276@\304\316\214^|\214\313r\001`R\260zcw\036'^\177IdG\011\324\304\203\222\363\007\222\363\223\215\012\367\031\303W\3037\000\000\370|)\307\024?K\317%\256\313F\360L\033A1\004z\261\036\2564\026\346\300\315\017M\313\341X*qN^2b\202\312,\272\220\227\205\214D\212)V\017X\226?\321\306\350\252\013\273\027\353\247\224\231\306\237{\352J\215\236\314\233=\007q\240\211#S\323\256\245e\031\254\2560EK\216\033>\277\362\327\242\2240\000\026j]zS\345tG\321-\345\312i\302\340q\024\230\232\351j\325\310\234\303\177\270w\000\377\370\205\315\337\233UJ\234\322P\355\013\036\002\245\265(;`\352\317+%\372\\\301m\204\365\322\212\212m\302\305*\014/%\264\241A\246\242M?\306M\012-\227\360\177\226\203u\336\020~\256\224\2061\302\363\270\021\202\222CUQ\254\370\202\302]\261\270\000\331\375\004\304\267L\260\0370\250\027_(k\221z\215\362\245I{\204\315\345\240W>n\250\367\034\267\017\034$\034W=6\347\232cUE$$p>\354\020O0\361w\221\007\010I/\244,h\354O\266\035\0001(\211A\377\245\014\343\3372[\215c\025\270?\267x]w~qpv\321\375xpvr|\362\346\274{\376\341\375\373\263\243\363\363\343\323\223\312\226\274j\367\315\333\323\227\007o\317%\020\337Q\312\336\347\322\005\016i\005\040^\202h1\311\306w\273\325\243<}\357\036\244?\355\247%\240\351\2424{\317;I\262Y>\274kR,\331^\224\376i\212OzqA\342\253\011\234+\350\311\237S\0023\244\366\353x8\304;\234\303g\317<\035\363\360SOU\355c\257t\241:\276\224\244t&\040,W\264\325Z\367_N\270GJ\004\275}\261a\303\245A\243\\?\253\040\267FYb\357\200\3479\341\253\035\226\307\\c:X~\227\366\330\210\206\321,E\335\216\212\2328\350\312\250U\3236\332U0Es\210]\227\304\331\022A\237Y>]\014\005/\2621i\020\311\276\010`\375d\002\312"_buf, - "\375\360.\210\230\006!\343\006\360\327\013\370\252\210^(\220R\202\343\217\243>~$(\370\005\312@c\026\360&QB\217\224@\020@\261d0\311F\001\306~OZq\213\314\206\300\002\000\320`\202/$.\343\241\010Q\250S\345\233\223\017\207\034\335\241N\253NJmT\254\000\240\277\310\345XP\253s\013\003\312P\336\213\320\335\037G\212B5\340\201\262\367%\275\340&\272+b**!1\3408\210\206-\014\006^:\022\206\236\363\3437\370[\243\000\203\340\336\235\037v\377vt\306\253\270\031\023qPZ\207\260a\266\357\276\003%\366\343\332\252\240N\026\364#X<\\\244\206\275a>\216{a\177\010z\021\006\001i0A\244\013\232n\037\227\216=\312~\311`\357\372\007\241V+\342\245$\203\264`\316/\317\216\016\376\202\306\351\323\356\253\243\227\037\336\2749:\023+^TBp\036\3149\332C\307\237>\177S\003\327\331\247D\200\361\200\324\261\356\006\367\301}\330(\326y\336\376\212\306|\301d\224\225\342LT\0165\343\270\303X\330=\366\300\306\361\002f\020M\243!~\357'\024\342\326\256q\223\244}\330\303\335\353.\236\272wX\303a\326\226,\343\343\361\311\253\323\217\347\015VI\362\307\223S\240\213w\007\177\327\271\246f\01499\355\312Z\013?\011v\307\221\363\352\350\365\361\311\321+YC\251P\024q\374\250\375\302p\200\026\337\036\035\234t\017N^u\337\301\037\345Cp5\360\216\306QY\251\353\374Z\220*\213\305s\360\372\357\257\336\276UL6\007\203\333\217I\212;\232\021K\361\205\257\004\373\244\201\361\343hF\033\302\201\"\273\231g2\014B\351\\\244q\253\222&4\232\265)K\245+\335R\306At\317\217~\016\032U\357\264^#Q\037\012\232\376\231\311\202\252w\024\277\352z{z\362\006\272y\373\366\345\301\341_\002&3\376\215L\316q\277\210\221\373\376\350\357\207G\357/\216OO\272\357O\351\256\353<\220_\217Y\374\345\342q\273\243g\325\002\315{f\261\317\343\\\267\275\377\356m\356~!\313\201\311'\237S\253\237\0174\305\253Y4\211\200\003\304\347\311?c\253\316\373\277\235\036\277\012d\310]\336+\373\307\373\012P\3325\025\346\037\030k\366\376\364\374\370\357]8sN\016\336\236\007\332\2331\345\330z\234\245,\301\001\207\004]r\003e6\354\237'W\007,\270\312\247\317\216\006\230\252h\312\353\235\343/\253\016\032I@\272\231\322\327w\361\010\3018\027\231Q\3259\3157\244+d\030\210\366\332\265\222f~\1778U\225\256\0340\027\217\002T\201|\355\015\255\331\337\375\202\373\204\262O\032;\260W\0051\034\277\233Q\2501\371^\224\013\177\217\366\237\372x\374l\226\212x,E$\016-Vw\263(\236\245iA\226\346\343\357\002R\250B\345\312q\203\262$\340\323\353]G\213\"\342s\260_\325z\301\212\303\254\366\254fT\340\361\304\350\322\351\252\031\0343k),\301\031\217\314\006\2650\3516\377\245\221\353\357\012P\177@\003|}\373f\222\315\3062R\275\363\3351\324:\007y\220_\367ahY\330wW\330\216\002\015;\312\363Cr\0115_r\313\376X\320\364\362\316.\262)\332sD9\375z\320\020\212\250\037\014\"\310Z\370x9\024/\230\325\256\361\267\332D_\017Jba\005\234\261\326\245X\223\242\266Js\244\246iD\252E<:\020\366\021\316\226&\271\216EVzt;\236X\267k!^\012\210\346jT:\212\244\357\274\344\304\367\337\240\243\305\351\024!\306y\256<\377\007\022\235\270[\311N\316b\306\257q\316\234s{\243L\260\201\277\203>\314x\307\017\034;\303\341\307(\337\3319\035\\`xW\346\215\210\177\272[0C\326\031\310J\034\370\210\215\346\321g\371!\005\344Q\000\040)\205\234d\323\013\214\247\237>\306\324\037eh\320\325\020\335\316\345\020\037cd\216M\375\243\220|\234Z!\324j\015\274\012\356I\226\316\263\275\202\247\217I\242\026>\202\247N|\350\261Ax\374\010\021\267G\204\377Pg\236\027E\315\200\270c\3764\220\306\330<\360\237\023\274%\347\332\347\362\227\012=\346?\275\250U\201\034E\223\341\335<\220\364{L\036\313\304\212\234\243l\357+\361\355\244$V\347P\374&?\330c\023\336Su\000\363E^\320f\356HG\344\332%\024\026\304\213>;\253\214\221\032\250\026}\033iK\314l,\235\"\0341\374\254\001\206\305\011sN\207\022\221h\040\346\360\257\320\273\303H\267\347=\220\261\372\374\360\010\003\376\207:s\316e\374\243\036g\343\207A1b\332ah\342\036\260\321\334\011\354\345,\031b\264\017\016\357\222\377l\270c\365\024h+rg\241`\242Fyq\265d\037\024^\201Le\011\201\274\215r\236\255\2462\222\224T'i?r\361\245_\"02\366H\032\007\245\237\221j\207\272\365J1H\314\012\335\245\344\320\337#\013*\3534\252\252i\212\362\030\212\371\342\345\253\340#\336\220\202\306\013\034\243\237qS7\213m\205\206\332'\372\230\242\313\214m+\2014\330\352\321\320T\352\015\255\012\204\354b\311\264]\200\207\030\263\236\307}\340\265\302\305\330\361\021\246b\011\312I\3725\373\302\363\000\0111\325\216\261DJ\235v\036\251\352&\315\012e\332\327\331\344]\222c(\032Y7\017\235\007\200\027\363\234[\030\344&c\022\263_\346$\230\020\374\257;H\375\222m\260\344\227l\011mt[\017\224ca\004X\010\275f\024gsX.\276\224\223\217\020\307\320_#\277\216\373\3748\314\265\225\306\345\007e\216\220Dq\366\360/E\373\320\265m\212\255(\024o\025D\241\350`\224\305H\243,WLP5&\343T\015\365\344\016!zJld\317\040\020\214\3126\224\354\310\255V\215\270\202\273k\252i\320/\375Q\246i\361\020\250\374\327\2563x\227\302\343q<\234;\345\356\312\332\371\240T\247\362|\027\357\2040hz\036\200\030\217A\304\320\017/[\346\327Y3~\"\010\016\230\267\304\315Q`\321;C\214V\346\031\221&#\341\220f\026\301\270[\312%\354\310\025\267\033\030AmG\"\016\327\241ID\314\031\260\353`\335\370b\022c\353[u\363\353l6\354\263\265*\224\016\314]\243%\224\342\265\271\235\251\010\201\311\250&\327c\006rQ2\356\237\001\253\026\371[E\334H\372\305\331\220t\302@k\336$\275:\217\361\350\330\365{/\350\2060\375B\306c\3612C_I\001U\232\203\366\366\370\251\220\345D\273\040R\346\315J\235\233\311\243z\273of\022/\314;\203\237\316b\231\273\000\225M\001/\204\236\002\367+\347,\267\342\274\263\371\274\215\376yW@\330\331\321\177\207\302~{\"\270\243\342W)KC\253\302\267\232\320\365\337\002\037\354\274n\220O\230\322\011\313\274\243\024\005\015oGf\2423\323\203OF1\235:\226\345\211\301\355x\037\3462\007\256uv\317h\310K\313W\031\257/EM}\236\332\222f\314[\362\311\242\026\261,T;\321)\307\013\367\351S\265\334\201\212\345N\222\277\004\241h\242\256Z#\260\337\306\3601\205\344.\266T\016\224j\252>\233D\335\256\227\012\014j\0058Wx\005G\334\023>\302oKK\\MY\016\360aLL\241\030A\276\237\336\321\016\343C\234\304\277\316\340\010\204OKK\367\213%"_buf, - "\021\034\304\306\022\356\271\202Or\243\374\316\216Y\262\240\012D\266m*\352M\262\023+\245k\271.k\324-`2K\243\302!\364\232\214\333\276J\362q\226\223H\277\263\363z\030]\345\\\314R>h\373>RO\260o\312\220\213\001\271\272v@\275W\275\246'Z\272MG\002N\203\2670GI\013\337\205\2514\254\303t\277\371F\320R\001\031\223n23)5)\344\276\202>J\207&\224\307R\351\326k\265\254\036\261TN\255A\253\360\205}\3169\207\202\335\331\223\040\317!2j\312(\250\310\323\350\234\260\370kEr\350\345\216\006\315\341\007ku\337+\302\341*\021\202\021\244\014\235\275\253q\305b~-&\234\024\036*\264\374f\222\322\343Ap\227\315(\3224\3761\011\204\007\007\3465\032\217Qm\307\357\224<\207\374\274\2572rc\242\327A\301l\214.\336Pn\202Eyq\231\273:\001\363A\017\"L\371\013\177D\350\320\301\022\241N\003Lr\205Q@\321\377\212'\337(T\262\2065\330\360\"\303\376U\277$J\335\216N\345=\040\007P\311YBU\332\200\277\314FcR\244YE\304z\023\364\236\333\251\316\317K\274^\\\214\020C+XH\246%E\364jN\037\232\015\350\325\3619\031\201\344-\376\271>\014\032\237p\347A\275\005\015O@\346J\222\\\327\0137\356\012}vvz\026\006\213\330\216\360\210\357\362\220\251\203\012\236K\305\237\220\365d1\220\357\246\376/{\357\336\327F\222$\212\376\335|\212\262\372\036(\031!\203\335\3233+\036\275\030\260\233\035\014\276\200\3333\307\355\321\257\220\012\250\265\244\322T\225\214\031\033\177\366\233\021\221\357G\251\004v\317\336\3379}\316\216QVfd\344+2\"2\036\016\217T\263\023\315Mg\034K\225BEg|\233\320\255\032E}<\377\350\327\265v\311\001\237\332p\016\342\032\3420\207\370r\020\364\366\261[\036p\254\032\014\201\353\341c\007[\235\\~\3201kNy\335g\231?\012\265E\026\336\305r\241\225\3674w\227\376~\353\212\340\040\377\004\243Og\037\262\351\364\017\2316\364\202\241\344\211@\266\200~\215\362dH\224\263J>\300y\216\270B\025\350^6\271\0041\035\352\036\200\213O\3118\024\266\321!\0303\331\236*\250i4\316\013n\312\231\214$\350\216\001{r+\240s8\035@\005\334\346(\213\034\015\266\317?\222S\240\363p\313\327\344\025T\"\226\302\236\344e\361\364\333\361i\326\331/\037O@}Rydp3\341\216e7\2578\306\275\036\315\023\2103\214%\352X`\333\246WF\275\007\200\317\316?\344\013@\233\205\\\001\374\346.\226ni?\251\022\215\241V\205\036^\252\2573;>\241,\202\274mFy[\251\223\315\017\261]\261\263d\206\336\201^b\255G%\227\032\276r\276\321\024J\2247D,!\260\032\274L\341\227\373\273(\276\220\205kP\360\343c\3679h\317\367\320\263<\365L\230\2760V^<\301?\333\364\313\363x\366:\236|~X~\251\213f\326\331#q\315\203\266oYD\005\310\212i\252<@\341\316\337\333\\\331\005\267\242\336\350\263\"a\\}~\223\040\265\"\205\227\346\223h\017\000k\244C-\035\213\303[KC\347\223\017\261\201BWm\304\310!\245><:\"\323\0330I\214\266\225\263)N3\317\006\344\307\020\373}\040r`\272O\374\342\031\357\222\363x1\237\353\256G\0225F\344\036|\007Q)H\342)\255Q\005y\221\364\370\217\332\035\\'\245\367\350Z\035<\342CrEcq~\233\365%\344\273\272\216\264\241pA\317\333G\015u\302y\253\035\0268\253\347\354\343\005\223U\340\346\004Qn@\301\373/n\243gh\211\311\356s\270\213i\215\301\031\360\")\271xsY\244\251\367Q\024D\353M\322v\302kW\3611\215\3033\007\346_\014\267\325\350\231!\366Eq\306\266\022\023\015\360\221.\264\227\234h\025\250\023X\335\216Z\217b\257\316G|\017\342\363\015qXi\257\324\250\235p\222\356\267\224\207\214\031\030\024\271gE\235U\260\004i\034\201T\367\250[\307\305\337\014\037\244\347R\011\257\245\266\200\036\365\235\261\037\\\\\344>h\260U~\262u\344\326\262J\250\376Z-&V\326\266\257\333\026\006\040\303q}\376\022\327Q\237\004Ml\032P!\233X-cZ\033/\204G\333\221E\003\026\335q\363\261\262\267\034\304\3001\011\261\227=\332\364M\226\330\220\016;\362\213=\016\247Fo\361\0035\227\014\373\250\260.\252Z:]_\037T\205^\371\303\335\340\246\023\252Wc\014J`\360A\007\272\364J\354\365\271\035X\247\242\271S\260\207\335\267$\002\257\010\025z\200\243TD\302\005\232\004\273mM\226\201\040\016\340\220\217\277!\223\230A~6\2752\245-\000\232\262\245+\001j\022$qL\302\306DI\220R>tD\242\353'0>.c\346\023v\025\006\205X\321\217G\226\215KR\367\361\015\032\232\206\211\306\247\265\015\315\037\220M\363\344\252\337_\372qZ$W\343$\302\337\3210K\256&9d\356B\213\305\360\327\214\375\301x\202\250\265\366\366&M>\254}D\023\236\262\325\250E\372)\253\326\252l\234\256\015S\272\014\363\242a\323\362:\031\3467-\335\365\207o\036\3326\264\033\367\362\351m\001\372\342\350\351\372\306\237\243\363\233]\030\211w\361\347\255/\337\012\205\335K\360\224[\020\203\367\355\273\235n\247\267\371xum{\371\311\357\277/\214\314\0229\220\356A\272H0\2104~S\357\301L\303\364\027k\364\003\3678\034\367\211\235\332n\276\253\364\306\014\325\024\263n\254\233\245\230?\367P|\364\014cI\230\276\377@\330\313\210\325\014\377\037.\213\014XO\032\021\253\373\303\017|p\202=g\274$}\372AuJ\360\321\251\222\343\243\276\261.\251L/\034a\016x\263b\212\021\336\261L\230\202\316./\263O\312R\224}\021\270\306&V\204S'\342\3004|\332\254\321\017=\211vL\377\264;Pl\040\036\353\215P\357\376\303\017\270\351`\373\351v*\236\267\265\317J\322%\370]\271\334\357\214N\336oF\0120\333\315\371\014\002K\337\306\034\357\244r\355a\030\236?\360y\027\247N\203@Ek\021\235\251\230\346XlI\334\303X\002*Y\216=\026\276\207\311]\371}\262\302Q\375au\025\033a\345;\370\237\233k\214\017D\240\266\254\205X^\216$4\366\375=\220\035\204f\302\022=\013\040t\246Vi\034\242gq\246\250\322\232\206\264\314\341\006\325\370F\243\272\010\000\353\010<\241|\207\342\375=\322\266\247\350\221}n\023r?\254\255\261\037\241\266\276\315\243\301\340\273\310\000D[\345R\202\021\343\222\003\223\335\031\343\321\227\231\354d\214Fb]7xS\261.\332!\344+\256\235o\232\242\317r\277p\222*v\002f\362\201QZt\017J\177\3217\225I\212{>R\254\016\252\354\304C\262m\260\034\036o\262i\214\007S\250C\205\335\311\360\014g&v\010V\264<\035%\020\360\322K\323>kXi\345\261\230\251N\264\022\255\200\026<\226s\377K\204\000YYk\015b\331\021x\201\230\274n~\040\315\033d\332J\013\014\210\015\321\375\305%5\255\012\370\322\2576eMT@\030\225\010\027U\0033\261\240\376N\253\3738R\025\212\224\367eVY\326\252\210;\245?H\252\364*g\\8\357\214Q\264\033\010\373/+T\311\025\356T\031\205\241\366:\"\363s\363\336\261H\232E\200v\334u6\310}psyw\015\004[\235\323\203\006\203:\322\311\357\246\"\003=\233-P\027\211\344\330DkmI\324\315F\220\232\256\026\302r6\027_#\014.M3\357\231xH\235\243\317=\341)\177M\213\364\243\014O\315>\210\246\260dz'P/\260\272\333\333\261\004\247;\220x\2311\002\247O\325\266\364+\201\237\313\313\356\031\321\353\350\305\274\356\262\2307\250\267,**>\333\235\226G\213c\034=\022\255\331p\2512\207}\307\376%J\210\215%\271\203\214y\374O\330<\222\350\021\255\213\275qX>A\312\033%\354tA\245\331\007\343\342\030\277\021\373\253\2700\301\370N\322\233\267\304\3150\314\011>\0351~\300\305w\311\000+\331I|\202Rk?\335-\031w\274\352\213S<\2733M\246\222\225j\301j\344\263\031tS6\253\357\304\230'\017\177\242d\016\342\343\"\201\327E\012\341\255\265\026\352\344\2106\362\336\342\007G\266e\344\263\276\345g\302\260\023\331G<\272\243\305\345\362:\227#-\367&\335\365\311\365|r.Q\242I\270\223)\377\241b\362@Q\026K\371\214\335\264\262:R&\3121\204\247K\264TR\200$\256\334\243\347\367I\213H\007\231\2461x\342\324i.:jE\024q\212\254{\237\216\325\332\216\322\017P\013'\266\275\347\006\224\323\001sJ\006ty\211T1'\2239I%%R\322\020\216\341v'\225\021gp\025\024*\262\225TN(\271_\036e\252+\366.^\"\374\034\366\3041o\265hZ\371yUU\254n\371\230m%\010\025+\266C\327?\314W@\340xy\210\260\203\311\360\376\040Z\030\2072\026Z\021\325\216dz\016R^\217z\373V\313\247}\221\030y:\320aJ\305\213Xw6\242\040\303\357\223\264\356\263j\215\227C\311I\316\334\337\375\373\304\224\246|\371\037\310\363\211O\275\036\335u\212\353\263\246c;\212=\314\234d\210K\301\021\213\3253(Z\273\206\347\016\363^V\017\330\201\"\366\343\013\014c\273\255\026Q\2261d\371q\022OA\374\013\347\371\036\314\320\352\227M\211\016\243\013\360\264\345\037\304\324\376A,\252\376\232'\330\261y\374\264\265\252jR!\314\313\017\360\017\254\241\350[\374\306\266\332\373\036\326\276\243$aw\316\233>,m~\251\242\3577y\331g\040\272\352?r\241\005\243=i\026\243\277Y\017\200\233\331\361\004\306U%\343t\314x\032\243Nj\274r'#\306\364d\325\365xg\311\347\025\353\004\345\306\\\007\221\021/\335\312u@\261\277\255\\\035\236\212\"u@\324\266^\302\355>\303o\336Q\335\233\367\320H\314\006F\003E\222U\224\243\212M?\346\237\003\203\210\202\214\"D^1`\020\331\252\215\222\361\3050)\331\001a\325\311\256\002\252\316\300\222Q|$+\306\012\303\260U\351\0264\305\320yG;z\030\3427\020\250\342\010[p\014zn\331\026\344:\000\000q\264|\244\266/\330\261\357\220x\342\357j\017\204\235\363N$\013Nq\030Z\011l\243\335\342\252\254\307i\213\267\2139\304^\357q\324\216\261!\000\340\007x\3070\234\244\310\326x\260y<\226\3370[\223\021\205\347!\250C\367\347\367\305\373|Q\244\315X@tI\002\034\022@$N"_buf, - "\224\360\230\035\253\217i\037\341ly?I\321c\013\007\321\353\341Y\245\177\354^\370\030\250#\376\303\234=\222i\317\363\017\351\204\034\3416\365]=)!\260\021n\354\"\271\201mZ\002\0037\315\2049\321\030\036R\240\030}\252\300\221j\004\337>f\011*\234\030%/\2519x\2363\214\332Z\247\260\005\264\031\344W\226\201\212\317\226v\334O?\245\246\273B\215\035\003\340\026\210\241\007\375S$tVi\320\241P\352<~\030\377\207}\370h\371{\364\024\0021|~\267\376\276m\207Y\243n\3613\273&6:\021\377\013\372Q\276\222\012\007\312\021G\3726\306\031\026}\2306s$8\365\012\023\236\351\221\343\021=\206\317B\266\212,|86\221^g\225\220*\211\247\324\000\353\310\021\277\305\373\010\336\264\236\350K\236%\2723v\035\333]o\213d\012F\340\025\2547p1\000\015\367\224(\243K\273\013V\347%\244\025\274E\203o\231\257g\220\027LV\235\346\023\214~\310\357\371\210\374\374\210\252\362\367\035\336!\000`\307\006\202P$\354\262G#:\266&@6%\371\315(\347O/B\276\025\022\223\202\015\330Lx\006\246\223\331X?1x\260>[\021\342:\260\250\010N\014Y#3\330Lk\242\300\230\307W\237b\234\015\363\324\232fF\254\337\327\220\022\361S\314\367p\324\016\207(\031\340K\316\332J\040\201\230\270\031\255<\210\324\352\311\212\0357\302v\267\334\205W*v\377)5\031\270\216\303Ko!\027\207\363b\22740F90\243$_\020\261\024l\326'\031,\000[i2H\037\372\351\2256B\242y\207J\360\010\321\005L\027\247\311(Z\374=\3218\253\274\205\007`\342\340#;\210\321\016F\364c\177<\237\201.\312\216\254\010\236\022\364%\266\275\340\214v\240\203\001\001\221\222\241\3321P\040\022\001\372h\244\332#fi\324\302g\270\030\342\253<\332&\244\361\361\255Z\333\361\373\260\301\177L\024\255\254\316\300\013N\207\341\363\245V\252\240h\031B\253\200\320\242\317\235\001K\337\251P\231\321\316\250\035\325\346\320\036\246\230\2706-^\343;\037\264B\273\267>\312\024\375\374\022\322\247\366\266[Q\040\3015\364k\300\360\031\313\005Qp\227F\311~\214I\224\247\267\327\023g\037\021\344\217\265\321z\307\354\233\011\240QM\"\370f=\011\352b\365et\304.\235\272\316\356|\036\207\366\254\341\002m\220\231\317\332\012%\325\204\376Hu\263\023=\255\2355\233\210\261\343\015z\2315\333\227\320\376\217\035\3718\322T'\033\244:\321\272F\255\311\334\2761\004\332\024\007@\273\006u$\363Z,\266\3260\246\332\0055\375\034\027^\205\373\355\276z\224\374\350\270\245\265\310\335g\243\372\321\272\013\206E\360\262lRC\256]\000$K\010\032D|\022\273\301\215\032\310\352\020\037$8\240\216Q\310\331\040\203\3751\040hw@\307\272\020\260\273\254BZ\331\356PY\034\040\231\372\025\260i\217\326\030\341\274\210\216\246\003\274\276\036\202\306\263k[\243\336\336\3368\365\236\371m\312\264\250\221\032x\351W\034\323\020\327\030|z*\367\306\0075\264\3376\373\203\265\351\321<4\012\177|O\263.c\033'U`B\015d\326v\\lx\270\344o\201\325\362=\321\022jd\235\255Y\366\3046\361.\004x\225x\311\241\205y\001\001\266\255B)1X\341IB\207\337\341\010\274\344\201X\011\273\324\277\365]\"`\352R\375\362\003\317\214\204\036\266\340\363\312Qu\310\005\360\353\026\223\216\214\372\207Nt\224_e\003\214\360\336\201\364D\340m\206\277T\2172\262u^a\244)-\221\222\3548\216$\363\316\003\270\222\252\205\027|VC\373\230\025\325,\031E_\265\306\2244\35121#G\213\232\024\277~\302n\304A\252\305\036!\363tI\244\240#\352u\236Z\204+<\364\271\373\015\236.q\002\345\003\374\334I\305\235\213o\236\306Y\222Q\206\2548\006\012\377\300AFP\316\031\011\316\272D\331\236z}Q\324\364\207[\3333\300\361W\301xu\200\242\324{&Q\275\262\255\325\207\000_\236\2631Iob\240\021\344\251\332\216\316U\214_^\344%\025\336qw\324\214\013p&\306'\037\034t=\375\327\365k\220\250\355\271\023\3066\201]c\371a\363\3051\355~=\2677\217\336Ta(\354\304\376\240%\252#VZ\2262k\365Tj\207\332\311Y`B\264\336\230\314\234\333\367\353\271\347\214\005HE\015\201\330\002j\264SG&\334\003+t\220j\316\325\337s\211\025\030V@\227\032J\317\301\245\220\007t\262\020Qx\236\357\004\350\226\333\307\233\035\227\355\322\372\210\365\037[ov\254\015o\350\342\\,\304\306\251(\012\223\243\223\303\240\3142v-\3255\312\014\255\234\2277\341\240\037\271\373\332\2739j'\200\353\257\351\3143N\376\215M]\330\361\326\347_{n5;\357p\232\202O\255\001\350\315\241\205\301\214\344\375\355I\315\303\216\260\314\320\322\254+\235\035\020m\203}\027\032\277\360Mz\327\031\020\253\377\306\222\201\306\032{\251\235>\030zD\325\343\203I\352M\325\211i2A:\000\254\355\352\327\017k:a\255\372f\315=\037\340{4\262i\253\304p\336D\330\255\311J\205\351I\256)\035\016\232ru\242\213\231\320\356\336F\303\334\006\000J\337d\200\\\027\005K\027Kp\223A\032tH2o\015\300{\040\371\324[\007Rm-\373\366\250o\247o\212(|\255=jp\255\341\212`\024\340\300\305a=\260\030+\305\346\347\004\302\266\210\3040C\230H-\254\035\306\203\241yS\000\015:\352\354\254\0129\230\272\2033\217\272\002\204z\232*\002\210\317!\242\341Y\364\337\260\332\235\246#$\3566Y\266Ys\015\372\370u\355m\341uR\224\251\212\341\247!N\301^\206\235\3508\307?;\321\3315[\324\275\254\030\314\262jw42\013\316\030\221\367\310M\010\037\262\241\331,\276\354G\325\210\035lh\355tQ\225\257]\221\302k\040\033+~3_\253\270\\\344\254\032\004\2152ZA\320\317\020\030\347m\312\246`\316\304\315!`v\337\266n@\2740\350!\256\214\026\032\031\263Rb\333\250\350Kn)\227<`\215E\323\367\017\343\212t\246\0049\244M\255\026v[x\353Z\030\031\315\016\301\332u\222\214\264:\276\326\270#DL)\376\242\365k:\232*{\027\037=\031\245\227\225\377e\013\343\2534\024[\365'/\266J\3542\250\030\326\271\367\372-1\216\030\333\244\313Q\225\024WiE\027\26119^\\\371\333\224\036\012\226l\205\010\242Q\272\263\303\201\233a\003\313\262\013\241E\2357\026\276\205t\034z=\223\221h\275\231\200\337q\244F\030\255\264\242U\3369\370+\256`\2427|JO\360\271\014f\311x\373p\342\303{\373\005\016\317\332\017\"\206\330\320L\355p\237i7\322\3005X\000^e\333\231\345\177\003\356\370h\272\274\330\256\211\312bp\264\347\301\037\353`\204/x\311\214\251\236R@\323OT=w\354o\357\336\307*\277\223|\325\225\224H\0177\011_w\370\240\252|\224\337@\250\011\320\351\231*w\260\311\343\210nG\255\333\026\006\243\225\2777\254\337`\263b\025\335\246\245U\222OZV\016\003\261\222\246\311\013j\023\315\376'\026\250u\3537\032\372Xe\223\334\356\377\3622\204\200\225\256\253\331\261"_buf, - "\260\216\243\210\225\037%\270-R\306\345\220l\003\314\344\2201\251`\217\000\021\033\257&\031\273\326\235\303j\034\314\007\354e_T236\3217\247\234\341\276\030\257\323\350P\237#B\233\366\205\313\257\026\023\017\336)4\210\254t&\"\361\242\177\225q\004`\025\025C[w\266\013\236\225\217\317\243J\316\032\236J\375n;\316'\020)\014\211\262\032\231Vjjp}\025\014\030BEJ\255\040cF\203F\313s\353G\313\232\226\356\036\375\005\333{\272\2763o\177\014\272\000\361\020{\201\311\222joQ\323?e\242\032\267h\201\3549\214!R\331\345\225=\213XZ:\340\321]\010\004dvj\330\3324\321AD\221\215g\330r\335\270\034\346g\177weJ\214\277\367L\201\035\250{RP\215\037\350\035p_\250s\034,\015\361\222\375Y\327\335\302S\205\244\334z\177\011\322\032\337\034\212!\030s\252\023\212e`\200/7=\252\016\243]\014UYE\376\320R\244H\036.-=\377}\327#\240\225\225\006\331\032\275b\000(}\307e\324\016\275P-4C[\206%\321\316\316\234\0313k7\237>\273]\263\271\254;\215j\322\354\335\362\275\326\303\276V\346\\-\270T\366\275R{\267\010\251\364R\267k\360A\360\\1\3566\360\234j\261\270\372!\377lf\012\235\263\242\274%?\360\315\217\304|*\021\230t\332\353\333\330j\363\333H\027MN\214\262^6\216\016Yi\037b\376\350\3021\301\356\013\305\016\031\325\226\375\222\201\332R\240:\006\262;\354@\300\306\354D-\002\033\215ge%\206\205\232H\360\017\004\027\200^Oo\3302L\363<\226\372\226\261\271\034\204\255K\246<\330qt$\216\003\271\000t\244\205x\355)\361\022+\202\200\233\277\301t\207\347\226\277\371|\376\2376\314\300\370\276\373~\344\250jC>r9l\032\350\021G\321\031nc\372\367Y\267\021'\305\033C\350\363]#\216\332K\366\204\265\0205Y2\002\306\273e=k/\250G#\307S\342\010\265\237\362\254\202);-\266\030\264\216\311\335R#\337\026$t\374H\326\337\206G\230\262\031*n.\205h\201\037g\356\241\301\316\376\231p\\\030iD\000\302\2243\312\233\240\2659\244<\024\266\311\246\302\305\040\315\201u\347\364Yl\\Y\370\375\031\027};\316[Cq\200c\211j\247!\015\251[D\270u\314\205\364\337|\377\346u\254\273=\232\315\033)\217\264\033\345\022\223\313\"\016\220\012\206T\010\346\305\341^\354\337`\007\335\373\236\347{\346\341\007_\355\037B\040\264\201\264G\217\023\036\3258\253n\0317)~A\326l\005\244\037\354q?\327~m\353\337\204\021;\204\2104u\354\327\020_\273?\255\212-\241\011\331\211\270\200\344\177\276\276\316&U\350\3330-\007E\206=\371\310\2601\366\270\006\001\371\346Y#{-\264\265\254\236I\325\341\345\027ax\216+`!\005\3741\273\356\372\204\363\226\251e8\337\331\021(:O\347\0004\216,\320\237k\255\207\370U\020\034\002\377\256M\3257\031\016\347eD\357s\206\004\377S\363\202\254\334\241\275\034\227\266[\350\026\025\373\3351m4v\026^)\332>\363\320C\375\001G\001e\203AS\302z\213L:&\361<\204\026:j\367Cms\311\226F\350\262~\010n\342\302\377\326\270\241\367\231\230\271P\036w\3032\331@r\356\014:n\025\265\367n\255\021(\333\320k;\246\336\255\355I:\212h\256[\311\326\354\207.\255\352F\030Y<+\363M\270\220\262zm\361\017\350N\237s]\360Z;\316;\252\"\261\226\377\366\304u\364\266\351\261.\027\312\233a\021\272\245\353)\200\334\200n\315\"^\304\316\266\325\324\324\342\360\331\277u\233Q3\321\325\346\320h\237X\012W\347\250\234\246\203\014\362\206z9rW\202\250\221\034\346\261\353\365\262\240\247\205\246P\363\233\210\365z\322\275_{\022#\341\352+;Y\335p\034\304\371<\201\262\236\343\265\351\277`\3027\012\277\177\224*J\216\260\335\374\256\371\026\032X\031&\304\026\3378\213\011\001PB\326\376\2469\334%eI4~n.5;[\321w8\\\272^A&\251\244P\222\\\325\340\256\332#\247~p\005=\230\304nw\366\326\361\231D\202)\217f\222\350\363\372\247`\035\333\321c\257\021\243>\002\254\331\025\346\266\036O\341\205F\264\010\031rlJ\335\301+\177O\316}\361Wq\337\221\377(^\266\315\274\277\024\027\252\327s8\202\307;\374\320u\257\322*n\207f^m\002l\251\037Ak\000k;4\353\276mR\334gs\024rK4=\340\367[\021i\300\274\272\352]\023?!\320\337W\350\336\314\312\224q\340\336\323\312\270vE\371\315P5\315B\236\300,r\040\020\240\202\002\237\370\2559!\201\304\252\250,\351\363\206c\220iEM\321\040\360\266\306\333\210T\210\206\350\036\373T\243p\251\211\206\304{\013EDZ\204\341\302\351wE|\344\264|Z\024\207\261R\312n\217,\357gK\261O\363\321\273qW\240\265\361\203o0\352\300`k\364\031.jX\237\177\254\357\336\356\370\374\333uf(=\300\306t\336\001\202]\350\025\337q\037\3516\022\262\331\202\036\222\210\022\243\213\232!\271\303\015\362\335\254U\261\205\254P\264G\275\216'\214\245\023zD\277\331r<\201j\264^/{-\324\2457\236\211\031\372\322\376\354\245\260\030\273\204\354\316[\035_\300\024\376\221\341U\347H\217\267\001\311\354\335p\334\037\321Q\264\325\202\177\251>\226\354\264\274+\371\231\375?\031*\263c)\276\356\242\273\360\"g%\222\377\320\246\243p\014\365\272\031\363\012\030\236s\336\303\274\024\024\2549\013\362\002\000\023\276^)\307\203\3277\307\202w\357(_\356\352\214\206\271\262\330\307\302z\215\373A*e\373\002$e\265\035\240P\350*\312\353\374\006~\277@[8\303\322\001\304\211w\313\357Mk=wS\031\020|v\370\213i\336\035\012iI%\241\230\011:;\201\212\001\365\360\356]\271\2705\314\312\351(\271e\222\013Du\310\040\250\307\030Y\367\226\277\305\273\326\332/\255\367\354\177\257\361\177\327\256\331\240[\357\275U\273\312\020b\316\002\321\214H%\0117\376\222m\306\364\036*\237\260\347\004\030G\345\2330\016(\375u\360a\302\010>\356Z\217m\307\262GN`x\267\372\233\225G\367\"jmG\016\232\315\365-:\026\360\300\342\2670\366\364\216\006|J\353\303~\266\037\332?\354$M\343R\337?\237v\015\005V\362`\024xwnD\263\271xd\023\236\323U\024pwn\0215L\024K\307n\373\203Hj\346\231f\011[\205(U\355\315X\246F\251\017\344\3349\371\246v\217\376\267d\372&IF\364E\346=\361?\244\277\240Di\3210\235\026\351\200\"\317\000\213r\011I8\210\304\257\256\260\242\262J\023L\210\262\362e\345\036\343Y\335\366\014\310X{\245\254W[\206W\324\037\244\277\251\241\247\247W\243\323\357\241?\365U\243T4\026c\244\247\200c#\020\\\021l\350\260>\344:\033\015\367\040\337\015[\366\256\215\260{\235A\307\342\010\320\017~\002$\040\265\375U\021\017\344\333@\315f\016\354\316\212\325~SdUz\236\013\323d=[\022\033\260R\227\331\374:\375\027\207\375\256&\006\2716\2155#\270[\252\001O\212t@\222[\325\371\224\351\366xLkA\310^^#\267\2129\202\004:\255y\242\236&\241\213)\252\303\376}\313\257\231|\244]Y\265\222\215@\216\327\366>_\360\234_\277On\030=K\245H\226\024i\017'\017\217\033;\312#\373\034{\364\274\371\015P\224zz\302\215.a\253\347#\312\010\313\332\220\000e\304\202`\377\234\235\034\0350aj\377\374W/\014\206\253h\277>G`\304\344^=\302\320\363\360\242\000\305\374\331\362S;\026\245\035\"y\020;IE\334~jk\272]\030\331\304\204\241\006\374\304i]\217p\210\216Cz\307\355\340\326\233T.\025\2763\177\022\346\274I\225W\034uH\025\254Iq\222\302r\011\212g\032\361G\335\325\201lE\177\332xj\2374&\272h\203\306\001\301_\345;\275\351{\014\254\271\265\266FaJ\331\377\277\271N\252\350&\205\340\245\243\321mt\223L*\014\367fF+\365CfX\274\267\250\373\347\320\304e\356\244\371\331z\363(\267\365\376\262\325\325\367]\271T\313\216%\202\027\240~\332\352\240\261\357\265\367\276b\214\321\372W\252Mj\255\352\276\203!3\374\307S-\231/\003]7\260\244\217\270\040\337\216\200_\223y\270\3449\375\354\272\233QX\337P\365yq`2\226#5\351\321\266\261\000\357\303\334\251\254\306Whm\307\303\253~\371\242\325\243\223\2715\267e\035\253.\327\315\005B\264L\322\255yS_\373\240\304\3045j\337\350\221\310G\266mx\026:*Fwho\315\313ddn\020\2774g\276\321Z+Q_\375\202\221\233\017\337\040_\320\322\322\374\347:1\033\367\177\265\233\263\020x\001\353\023\326^\370\341\315\011\311)\003\374\015i)z\370\314:o\327\005,t\357\354\213\342\321\371\363\375h\357:\035|\210\306Y\211\227\271\224\3238\341]<\352\320\212\012\330\214\322\350\012^Yx-\013>\310\324\004\034C5\344\"\3607\206\300)E\276N\204\354\233mu\300\011\310\251j\214\230q\224hG\271\275\353OY\014\300\235\265\366\230\253\271\327\303\350L\034\307\241\226\320\232?\223\366\373\254hr\325\357/\3758-\222\253q\022\341\357h\230%W\023&Se\003\210l.SA\023\226l{c\036\341\"Y)y\320O\300\227\313\214\321\015\211\327\214\336O\331|@\252O\343M\366\374\340\325\353\032\275\202\225\225\232*\236\037\374\355\374\305\321\311[o\213\246pg\223\005\220P\003\306y\203\311\354\363\331s\023bK\313t\025\010\013}%!\374@2\031\036\261\341\010\365>\333\206\227\331\325~R%\220D\036\376F\326\014\027\017z\262`\333\030\020@0\370gx\250t\342\227$\375\352\031\306\007@\250w\226\2769\262\272\040C\033\322\312\006\276\351dPC!#\255\336&\005\\\002\354\236\306G]\217\270u\303k\004\205M^\341\014\343\233\002\224:\343?\001\015\202\341\036\347\273(\241\240\322\255Fy\310\0111\240\272{\221\317*`DTK\017\357)8\033\253\267svJ\026\356\010\033\005\372\360\325gw\300\304\363\324}\027\2677\227\274\246\330\372\364m{`\3252\004s\202sk\234\000\357\206\307\374\025\023\343\004\375\325\244P\266\263\272\274^iY\346iX\276e\304y'v\352\177\321\267E{\363\333:\335YR\263\266\243!e\033\254\030Z\273\274`\264\376\005\023vj6\267r\024\364I\237\030Q\211\037\343\350R\271\025v\007"_buf, - "}\264%\217B\032\225\313nV2)4\235\314\263\246i\030\\\035\206\025e\223\351\254B\034\370\"J\344\345*\372\207\040\242\3323\"\342\342\313\305A\254x\225VP\211\015\264\203\265k\014y\36130\367\3318\206\277k\222\331>B\312(T\360\313\313\321#\266\225\212\252|\233\201R\023>v\242\225\037W\242z\361\012!y\032\266\260a\255\300\300q\205\252\253\364c\025~\204E\010\276\231!\003|yR\234'\206\001\001\237\231\205Z#\367:H9\200V\247\325\334\354\316-y\362\3444\205\010\322\021^<\220\350\005\031\256\311\320\267\027\037\371\261\301\327\275\245\246\330S\"F\337\027x\273\\\333\360n\275\357t\302\331\235\005\007\374\244\030\242\352%t\260s\374\0362w\323\367Qk\230\302\025\231\016[\035\331\252vj\330)\025\275\237\316&x?\364z\207\223}\004\203L!~\366\273\316\270\335\217\322O\331\040\031=\260\367#\200\222_1\036\372\032\240-\204A\3018\214|\374@\004N\021HM\277ud\320\023\256\264\3766CL\325uF\210\007/\263\357\267\025O\341\202C5Gh#\226i\340\211\027\227\001>>\202\344\005l\240\255\372\253\202\317\221\2204\314x\364)\330\325\212\005\3428\205o\365Bb\255_\3522yC6\251dv\006\206W\314\365\332\355?vj\367\362Q>+\336\240\031^xzgeJ\025\353\223\262\217!\330\0308\324\034a\262\011\275Y\220\177\214y+\221H\242=\217\342+\240\333\321\033\361w\257\367\367\264\014[\025\230\375@\276\210\373vs\2347\355\005f\344\376\375\354\262\326\367\260\222h\302\360\014\250;DT\004\241\204\360\227\371e\017\227\261\023\261\205\000;\335I\336\245\203\257p\304\\3Fz\213\341\037L\013\336&Y\365\"/\376\232\336\202\210]\326l\332\017\242JP\246\0225\216\006\372\256U\3556\275\242\204\336\012vS\312(D\253\226\230\33788[\243`\373\012\240\324\\%V\247x\263\264\242\207\366\372<\275\314\013x\344(\252\346}\263;\360\233u}\360)[\240\347\013&\253}\323A\357N\206a\014\356-Q\310\015$]q\354c\206{\246C\374A'\202\011\2150ocu\315O\234\004\321\344\300}\277\303\366[Z\\\344%\005c\013\035\263\217\262N\333\347\004:\032\350@\344!\323Z\271\236\227F\033\266\356\377\234e\251w\313\361E\377\250\365\040[\366z\377/4s#Wx\273\040\207\335\305\3738\306v\015;\271\316\256\256\027\357\342W\326\352\276\001v\352\270;\331]\207\366\234\352\336\313\342=,\200O\220\265K\247yQ\325\012\031\205\250b\357\257C\321\3704\275\312X\0336W/\022\260T\275}\225L\245\356\001K@\243\215\306`\242\352\257\263\213\270\335\275R\010\210\017T\372B\264\212\275\257\307\243\201\206\267\334\323\012\321\272<\024\022!\214$\025\353\260\354\276`\017\251\352d\302\312\370\330\240%\211`<9<\356\016\240:\370\326\273\350_l\221Eg|\023\3119\200=\324\345/X\311G\306L\243\226\005\255\263\327\326Fl\252\327D\325\362\233o5k\257\2152\003\372\266\012\012\307'lZ\344\003Fl\311Z\300\250\373\205;\354\360\212\302\337\306\251\205\356\377\274\022\014\016\0056\317\012\275k\255\215\310\203\005\247\000\245|\217\037\013\010\254\354{\224\214FO\3060D8\016P9b2\204\313%{\372\207\010\343\336\356+\275{V\253q\357P\267\266_\230\234\263\331\000f\362r6\252\231\201\222P(\251\256\037\001\241\314/%@\034\177\011\212\230|VMg\325\\df\243\341~z1\273z\016/\331~L.\010\023|\353\366\343\201\237@V\203\374\212\014\330\025i\250}\252\305\015A\177\012\302S[\203\277\302\261\376\305\303L\315\272\334P\377P\323\337{J\357\006\342-f\356\342\010'N\375\250\356\317H\225Yr\347\315\350\027\310\323\254\212{\275\335\321Mr\013\306\327V9\311\2138\231LZ\377\342Wi\260\001\014i\030C\321\262\346\254\343\375\242*\326\356\353q6\021\3100\014J\306\016L\206\241\225\334'\024X\023\211Fc,\360u\235\010\177\225|\200%c[q\224&\220\276\343:\215\256\262\217\351$\232\314\306\027@\226/\243\000\"\034w\357\263\325|\322t)\350%#@kP7pe\302\353\021b\217\257\302@#\030\337$L!\260]\355\224\012,\312\3352|{\377H\270\310\272kIYs\213'\303!\023B\341\352\246yd36\207\200\212\275\231\016p\372\317\363\323\031_`\351O\035\230\245\001\277\324\251f`\201\321\272\3446\022\340h\212\302\007\3667%\241\220\014\370\205\304\264/!A\212a\360\221\360\220\302L\000\223\264\342l\204\022{Z\215\270:\334<\230\367\336\333\275\342\353\326h\201rVuq\016\217\357!l<\027-q\345\225u()n\273\026\231H\347\312\003\313\"_\206:\364\254\363e\224~\372\002\317\033\201%Y\303\207\003\177\277r\310\374u\301\270c\001x\273\346\252'\005;\303b\005\304\223\225/D\010BX\024\223\2535P\341\207wD\"\354\237\006\364f\000\207\206\236m8\215\251\231\024M\221>\217&\2571fb\215\264\260AB\310\230W\261A/\340\015\022*s\345O\355n\2708\004\277\0318d\241\275p\221\361\032uL\017Y\231A\222\020v\247\372\031\2171\232\372\203\225j\006\301\356'\3218\033A8Dd\317\200\312\302\327\0110\317c\040)3&\346&\203k\272\000\347\235\210\335\342\312g&\321\341\227\011P\256/\323\244\002\033\356/>\255"_buf, - "\205\240\027\327\231\350Q\361\230(\350\231F>\362a8\223\301x\203&\213A\213\305\301t\012\037\221\342i\237\363\011~\321m\027yP^\255(\027\026\216!\203F\024/\3160Y:\2300\202\205\277\262\306\006\305sl}\225\346\033\206u\366$'U\203k\234M\246=<*C\027\177-/\203\307\367H+\306__\276H\015\356`<\305\032\035\355;y\205\310\214\217\363\221\337\272?\356`s\215q\222\300Ck\306v!\0328\303\321\207\334}\310\010G7lW\262\025\237\241c\325u\3621U\337\2479\222\214\256\231\301aV\242u/\324\342\025\320;Y6\353\300_\005O\357@U`\367\017X\007xuB5\234\032\357\024o\3513\314\2462\016\314<\342\376\310\230xX\217\372\231\337b\023o\346\3324\275G\325\234\303\244[\337\330\260\375\313\000\241{P1\377cv9!\263\352\227\307o^\366\373K\246\203,\324#L\331\217\225xE\226\321\300YY{\305\216\010\345k\3313[:Q\234,oV}\244\374U\201\\\031\017&\303\263*\237\252\255\266\032\273\241\033\364\014-\324:6gP\313m\017OA\362\207\233=\320\250\3715Xu\351\316\245!\234H8\344\003\011\040\222\217\000]\040\033f\360v\206\177u\233f\261zC\370\233\250#\345\327\202\202\230\027\253\274Y\343~)\334\207\2472-\2370`\262O\334$\245\330i`\264\010;\025\2643\300V#uM0\210FU\3443\371\032P\316\340.\033\315\362Y\251Cb\344\271\342\203\212\331\277\335(\033\003c\232``\303\"\272\030\016\243q2(`g&\340\255\220\227)\236DFrgF\202\024\006+\031\221\311\015\366\247\000\267\273\2327\266pJ\\\216\322Q:F\177D\230\006\375\206\261\337\237\260\"\267\316\204\277\235xS\265\200\015\031{!\320K\312\241\275\254\3009\204\233\262\223\327D|\310p\335\035eI\251\236\3050\200\247\006\203\307#\261\007\030\212H2\356_'%t\366\002\227\263\014\205\311\326\335:\227\361J%\270\215fS\363Y'O>\325\276\035v\247W(V|2\204o`\321\025%q;H\0148\252\342\244\300\313\037W\220\204\311\001\037\212\251\341\216\214.\360R\023@\345{\220\004\251|q-\230\262\352\246~;\332\200\244\012B\001\014\001\222U\303\000\331\374\272\210\371\001\262\252A8R\011a\002\363\301\221U7}K\242\255\305k\365.\247\226\303\002\250\275\335\011\334\352\027\370T{\035\015B\325\237PM,}\011\024\334N\316\325>\017\366\241\235\005\003\363f=\234\351\344#\330\207Ad\344@\004\335\260\301\252\003\343\202\023\337\274\033\300$\017\276\346f\215M\357A\021\257\253\341\321\360\012\2629\243\355\207\324ZE\000s\341&\020\007\027\237\342|;\335\331\361\2020\362\347;cm$G$q\026\227\241\017\254\206?\325[\333\021\365}{U\3004\310O\015\242\002O=\252\350/\236s\241\310\257\236*\313\230\".l\250\347[zd\237s\230=/\276^\3507\302\221\345\025\271\262*\267\"\035\276Z\200G1\357@:\272,\207\374\222\332\365\035r\317\"\377l.\320!B\221}Yo1'\3051\223\310\365],?\326neYK\300\035\022[$\040i\217,\301\355`\303\325\332\010\250\272\3758\370\021\221]\271\350D\030\232\033=8\344\220W\022\040u#f\005\211\324\261\363\317\002\201\244\332\002\242i\311\0133*\341J\353S\013\262\015Q\326\363n\011\333\040`\316\226P\313d\264\022\240\365q\253\007\316\232C\353\202V\315B\370j/\371!\310\276m\245Z\011\300\312\012M@\227\217\036M\001\313\006~\322\355\321\202\325N\206\003\337\003\3007\327\266\216\266\276\223\332\2168\204\300\331\253Q[\372\372\014\366\3426\257=C\256\206\267\341qr\033\0327\314\200\335d\223\034H\212\246\206s:U:@\227n\005\301\3046\006\012\212\012Hy\250\007\020z,{6\304I\207\321\346~\334\340\241\313+yY\357\366\302z9)9\27325\330t\364\351\271E\250\346.#r\313\036\306\322e\274\275\364#\212+!\277\361Yy]_\203\255|\016\241\034Zko\341m\002U\251k\303\224B#\345E\331\322\335\260u\024\323\242\230\344\375\253YR\014\273\327u.\316\220{\027\352\276\204\252\201d\210\252\202\036\324\350\253[<-\262\217I\225\252\226\260g\307\375|4\304\272*\371\374\235\345\265m\242\253t\232e\215N\023`\251R\315\347\232\007\216:$*\217Y\350M\021\362cVTL\370\217\276jU\\U\214]\033U\0223\360\275\0237\320\036\270F\360]\200\177\267\315\320H\272\211!G\3528\327\260\352\325\341\350\357\255\255\205\313\272[\362D\327\326A>\306\340\266\011#*\261\337\243\026\033\030(\225}\321$\350\254\261\034\252s\347\014\034\017Z2\341\026\001\270PK\276\040\014t|\036\211\363#\002\033\210\260\003G'oN\373\307'\307\007\021y\254\326\326\343q\354\233T\335=>;\214\350\214\326\206\304\377\221\017\313\014\273\340\353\025\253R\312\276\371m\240{j\200GX\234d\215\222D\363\206\370\244\351\177\276\003\344\034\036\"\007o\263\311\263\247\306.\345I\004\335\315j\023\012\253)\246\226f\027\002\243\303\277bb\3428z\311$\322j\310\177\235\235\357\367O\336\234\277~s\336\377u\367x\377\350@wv4\367\253\210=q\266wzpp\334\177\376\346\305\213\203\323\376\341\361\213\223hP^d\240j67\343KxjG2}6(\322t\362|vyIq\317b\003\245N\264,\000\330\346\305y\221]A\264\251\027\214\004_\025\220\366f\267b\022\330\305\254B+q\321\254{\243\025/G_\343\350\371\356\336__\236\236\2749\336\357\277\004t\243/z\321\351\301\276Y\360\374\350\315\201Yrx|~\3006\307\371\337C(=O\006\037\026B\211\355\347\003\013%\255\210P\322\0128JZ\211\027%;\352\361<\352\030\210\002Y\336d\230\327\315\254\354\022,\264\377P\036v\223\264g\352\337A\237\362\251\222C\217\353W\320\347\221\251w\360\026\024\300\275\372\016\3563\255\363\372=M\207\275(j\334/t2\017\344K8\001\213\016e\036\320\347\243\331\334\005Xt\354{\267\311dQ\230_\026G\375\357)\250zz\213L\361=za\263~;o0\353^\227W\003\314\021\304\235U\260\232\040\034\244\037\356\"bT[\271\345\026\003\376e\321}H\275\311\335\370\200\336\032nQ\354O\036\344\007\367\367\015N7\241$7\340\003'\334?'M0\350q\316\002\200\237\036\357\036\365\017NOON\301\272'\007\0037nv\346\205&\354.\274\216\217\004U\000{3\3710\311o&\034\034\206\246c\302\213\3077\321\011}\354\210\022x\301\270\363\364\366\344t?\352Wz\241sw\234I^\300jl2\002\026\224/\365\267\255\367\025\217\330\030\003\256\252\206\250\326]I\201\252\276\3567\215\200}\006\317\017\241\371\300\346\207\227\371$\000\316\371\333\234\236\316\330\313\272\\q\376\272*D\200+t\220\333\233\025E:\201y\205Y#o8\376\200\254\315\214G]G[\341\025\205\000\040\210\306b\375\302\013\327v4\205\236Q\243\027v\200\007O8\275\003\307W\336\262\3611\220\011E\011P\011\037\374pYek\004\232\210da\256\013[\240N"_buf, - "\027\213\343\352&|b\2237\374\331\217\351H\323=\324\010:QSQA\311\337\263I\306\366r\367z\247\221\374\3000\004\377\227\327'g\207\177{\022a\247\"\342]\225\202\005\027\223\242\007l\376\312%i(\223L>\340\203\377\3560\031\203\311\305\277\322Q:\370\220Q\334A\276\377\001\010\355{\010\361\307\233\306\327U5\355=yr\225U\327\263\213\356\040\037?\231\260)K&<\024+\253\0025J\263\312\364:\033\225\377\234A\200\231'8\214'\323\331h\364d\343\331\206&\000\275\316\313\354\323\375\004\240\177\017\027|\017>\226\037\254\250\365n}\334\272\037K\252\201\330|\266\321\000\212\227\0135\240\335K\037\310+\333\244/\244\352\253S\005\316c\340\3044\323\303\301\340:)\036G\375\264\034$\323\324K\363\3460\033\352\255\334\231^0\004\374}\375\3313\264\005\324\272\010\305=\306\247*\311u\234L^sN\312\363\350`\350\373-\265\341\253\335\2756\230e\006>\037\276\376\365\344\370\300\304\366QV\356s\267\341\335A\225}L16\237P\011\242r4V\257\013\373\377\365\362\365\353~\0375\234\252\364\354\374\364p\357\034\357z\366\315\204\237\225IU\335\202\302\355\344\315y\377\305\341\321\301\361I\333\223X\023\343\307\3732^\352\246\270\213r\233\332\373\002j\3707\377\017\344-\275\273\312\331\260\277\324\261\225\016zz\214\251\305yT\353\250\353\314\350\037\302\26322\212/\001\214F29d\0109\203\030\317\207L\343\223\305\376\013>o\371(`x\277\2129\014\016\267\366\351\220\037\0324n\245\341\020\007\374\004Lz\323\032\333[\301&\022Q\364\260\213\237\211\2414\012\305\243\255\333\034\376]Vf\357\036\203\367q\037\342UB\2749\221\365\021\177\253\275d\024\0336\234z\247\313\262sa\032\275\375]\372w\214H\335T\224w\346\\~\025'\373\263\310\036\011<&\0357b\364\030\020h^\0005\347\373>\017\374f`x\303\266\300\266\040pm\014\340\362\2535\334'q\225_\223\232\230\372\212b\375\363\017\035\335\251@4\345\202\253\260\214\204zKZ\274$0\352\333K\246lv\322\307\024\016I+\211\203\"\254\2645)\364\372>E\331\341\351l2I\013\002\216\1776\201\212\025\275\317O&'\001\353\254\361\014\234\325\234\007\236x\003/\370\257\222\027\221P6\335\2114\347\335a|\315It\246\331\230\263\260\232\300\232\334h;\232;\331\022\001\232h5\3714\241\265}Q\215\355\310\235{\227\257'\367\021w)d`\374`7\222\373\253Y\003\236\010\317\232\345e\215a4\277\310\220\\\216$\242a\350\256\272\234\034m\360N.1{\355\334E1\232H=\255\211a\364\330*\350\365\006\006\353\253\303Q\227\225\333\210\321\272J\015\333r\267qA\2467\2216G\3324#\370\301(M&o\246j\307+P\303\224\335\010i4\007km\232\232\017\317\020/x\355\257\207\012\011\3138\307\201\374\365\320^}\273\211\252\352\255B\026\273p\021\245\257\007W\317\236.G\005\370q\271W\277V\207\311\307\205\236_@\350\022T\351\235\307UK\021v\213\346c\344(q7\020\345\257\255q\255\253j\205Ch\300RJ\317J\214\242&\177\021\361\206\322\303\305a[\3036\323\262\273w\205`\256w\336=\336?=9\334\357\037\235\274|{zx\316\244]\205(c@\012\206\314\223Q~\205\212e\323\216K\307\371\236x[\324\245\337\347=\366Y\217}\004\025G\032\202\375\375\203\347o^v\242\026v\335\352D\264\315B\345\357-3\0251\316\2371\225\027\263l4\354\363\371\346\036\266\370\330\222\226\220\253)\031\202k\036\2724c\334\200A>\236f\302\313r\027\040\354!\037\033\343\002\311\327\224\027\360\"\203\013\305\0330\0401\326\354D/\367\366:\230M\272\015\222\203\360\354d\222\037b\312\032\215\235\321\263\377cb\020\016\2243\310\365\033\262\301\030\3258\331&\003\355.l\321\313\331\204\202\010e\030\032\212\011F\303\254HQ\002\303\220K\225Q\273J\007\327\023\020v@/\241q\014\332k\322\220\311`#\0209\272\210\011>+\215\262\213\")n\237$\305\340:\373\230>\371g\302\376\377\306\263\2377\236\364!A\361\247\356u5\036-\351\320N\361\246(Q\264\025\236\342\374r\214\270K\016`|\221R>+\334\237\303(N3X\001\035\020\360(\030\032\001eB\000\243\342\017\026L\026\204\300N\262$\251\252\004\343`21\263\242\220\241m%\247\241\372\323UDZ\222\332\3045\242\037g\027\357~\262R@r{\327\017\340\202\335\207\021a[\333J\217\210\011\345\245\224e\354\247\245x\007\346z\302\304\\&\204\376\213\234\350!\012\032\023\\s\024W\321\327\2366\024\206w\243p$(\037_d\377J\330\254\332\320\330\301)!$\031\333\255W\030\320gZ\244C&\227\243\210\304s\363Y1V\301\254\356\303\024\207\322\235\3661\012\033\032\373\326\040\312\346\245\023\211\370\021#\206\026\307\021\006\200\016\3617)\246i\355\030\202\263\015\360&]a\034\345(\3171\256\031\214\014\332\026ct\336\211\022p=\322#\022\211\335s\270o\215\000Vi\375=\344-??\352\377\365\340\364x\323\371\274\001\237\341S\377\365\351\311\236\373\375\251\361\275\377\372p\337\255\363\354=)q\247\331\320\011\212\013I\346@\325\300\017\377\222\223\040\035\222\040\260\177\362\313\030\306\350\011\371L-c\234Y^\223\375\335\216\236\210_\217\341g'Z\206\366\354\037(\355\010.\263\023\255c`\334u\357\273\252\270n\230\210B\327M\353\367\311\343\307\2040\243f\332\376b\007h\215\0358\221\017h\230\322\3536\036dy\326\330j&x\202\242\307\217\177\237\324\245\245\3278G\236\277\325|\332\261\247\360-n\010\2134p\032\362\272\177~\272\273w\260Oa\002A\267\223\332;\231w\025G\261oO/K\020b\252|2\237\371"_buf, - "r\020\040\034.C\240\271\375CPc\320\027\343\215r\223\334\232\023\211\207\341\022s\013L\326\220\316\222nE]=l\353\217\223\301\311\331\322\374Y\324\321\226\017\033\0013\364:\316\356\350\360\370\315\337\350>\364\344\2253\213%7\356\271\325>\333\267\225\010\215E\226\0238\033\371%\316\306\240\302X\206\306\256\202\360\010\025\244\340\204\364\316:(\266\303\023Tec\330\021\"\003\235\350bV\321t\302\314c\353\013vG\200\016\224\207\034dW\326h\210|\030[\015C\303\007\255\344eT\225\351\350\022\017B\005\254\003:\313\362\013\205G\3732\352C~P\343\242b\363\012\264\016\342\2601\262\315j\026\011\256=cH>\336v\2437x\177\035e\223\331'\040\2132\310\012\2431<\213\245\016\254%&\244\025\305D`A\3659Y\251\344\376b\203\274\0322\"\301\3322\006\262(\031\025\3100\3517\214uT\032\023w\201\321V\330\334up\317\245\237\3203\252\015T\371\011\214\347\311\377\303\260x\002\342\037\304ua\330\3777\304\351\207\213\203\201\204Y4g\015\003\2730\306\230qZ\013^\256\254\371Qv\301\310\304`u\225\337\335\254:\334!\000\323\314\200\306\016v\031\241\337\014\014w\335\206s\016\213\201;iV\200\336\273@\0253,\017c]\030\363C\\)io\011\012\246\25149\360\360\313\235\274\274%>\331$n\321l\301F\341\263\325j;\301\037b7\015\232\231\357,\233P\276\263\366fT\343\265B*=\340F^\237\036\2748\374[\377\350\340\030b\330n\370\223\300Qt%F=\222\"\215\327;Z#&\370\235\303\332\027\257\263a\357\367\252\3053R\207\022\330H\372\213\033F\321]\266M1\230O\305\303\203\261\011%\332\006_(\301S\010\0362\336\353\214/\275\312*:\034\325J)\325\3619;\240\020\034\035v&\356;v\327\263\231\033\245!p\360\242\316n\036=&\221\207F\342\204\214\322\311Uu\315(\306\216>\213\313\313\370\365\235*z\017W\301\312\272'I\333]\335]UG\216\275\364\327$\277\375Wg{\375\337\016N\211\3442\331/e\300Z{-&\004@\304G\340x\342\341hD\001_\332\270\027\372}8?:\350\037\374m\357\340\365\371\341\311\361\231\351j8\247r\177\357\315\331\371\311+\356\361\306\211\333\273w\340\347\013G\342\375{\265\3210\344\177_\006\366\247\300m*\316?\337$\251\276C\\!\206\306\203\352\036n\204[\201\3600H\300T7#\206\211G\342\302,\004\214I\220]t\331\304\007r\210\001h\340]\331\264aR\270\233\244\354\3212\247\335\033&\223S\357\260y6\227\014\206A\"\241\336\036t\015P\335L\214\362\253l\000\276\302\214\213\360\234\226qy\245O\205w\366t\020P\337|\377\250\353|\230C\372\365\207\365n\300X\254{\236[\345a\375\233@L\004|\027\362\246\345\245\255\235#\347\210\315\306}\342\040\373\005\217'\326\270\236\361\216Aa\215\364\300\226\343t\234\027\267\341\270\226\252x\037\3231\232\276\340l\330\263I\366\317Y\332g\362\321\326\001\353\037<$w0\241\267\370\025k\244\0300<\306\020\215\252\214]\274\277\001\316\030&\253c\204_\202\364\2142\305\025\216\3130\033\345\036\366\014&\002\220\261\326\334\327zO\235\317K.\277\255G}\362\215\213\015\014'\031~\330\231\262E%\2251\011:J\013(o0\005Pm\356\350\365\007r\277\037\274j-\241\357P(6\354\300FD\315'\356PZb\276]}\011\340\347\357\260q25\322\300\013\005}\220e\010\256\220\366\354\350~\004>\347n\376\016\325\275(,N\376\340\023\010\031\020o\226\021Z\366\007D!\300\040rS8PL\262\005Z\315\372\215\274\316&\250\213\344B\310M^\014K\224\200\013\241xf\040\301\224^\302z\236}H\313\353t(\214\311\310\322\336\364\243R\013C\230\011\033'\214\221f.\233\370\344\223\243\3405\001\324?#\310\317\220\017\006\263\002c/3$Z=\367\276\341\352a\030x\237H\311\266\321\003g\200\002\311\264\243Xk\270\023\001\213d\264~\247}^\2136H\322\351\255\204\304?\312\300B\3657\033\244\207\346|\256\211\357\354\002^\347\024\240\216o<\014\033Uc\236\267\334}O\226/\023_*w2hc\247\243\254\222\315bj\327\211V:+\266\011z]\367\226\342\200\312\272\040_\024L\036\320z\024c\367\251\015\364\230\221\262\005\243\243\032\276\301X\221C-e8\005\313\364l`\011\250\335\016\316\267\266\246\366\310\264\245\020\204\226\321\007y\305\264\215\200\035j%T]P\361\317\246\224\031\007\011X\344\306\226\364\316\006V>\317e\014\267\217\341\331\000\011B\253\336\245t=\214\273\347\035\326\251\014\364v\024\324\247\301,\265>?~\034\315&\351'&\3252A\211\350\025u\366\370\361]\253_\372\355\271\376-\367\266\343\023\021\302!\225\375\203-\215\274V\255]#j\255\355\214\373\023\312\261'\220\014W$\\\324\331\370h\237\013\313\306\034\266\007\356\205\217r\240\360\304Cb\300>OO\255\323\000cN\234\024\204x\025\306\0320\321\263\330\"\241#\252\277\344ef\030\034s\317~\344\307VL\272\233S\325\235\215t<\035\261\033\222\316/\226u4\024\337e\253\253\357\355\2311\010\257\376\354\3509\246b\037\370.\370\005y\244\007l7\215o\323\250\225\261\335U\247VW\234\367\362\347G|\254CF\240n\010\017\235\267Z\214\265\3621\371Z4';n=~\013Z)\231J\347^O\0171\325S\201\244b\204\322\376|\347md\004\246\212>s\365\366\266\036\210\312\325\040\3308\333C\022\202T\277\002E\321\010\354\215\3159X\254\305\034\221\307\215\356?O\010\342\202\206\350\364\\\366\351\2218\352j\371\375Z\277\3264\321w\223\021+K\034\035\325D8}\3710x\034\251y\322\211\213.\352\212\032)i\022%\024\307\3047\324\374V\365'#\346\205\254D\353\304-\337\000\010\032\012`j$\312Q\337\011y6g\177H\203\227\223\347\377\265\327\357\263\337\250\017\216Z/\040\022\000>\376?Q\177v\257U(\270\300\016\251YB83u\013\314w\205n%Z\013\355!+ox\212h\023\251\321D\347f\016\303\336\211M\340J\337aD\034\233\247\340sb\360\326\016\177\3566\325\3244x\344\354\265v\305\270\223\213\377N\021\330\332\036\266\311\345\357\201R\327\225\224}\321h\376\237\352P{\256\006\373D\324"_buf, - "1t\377\211\3736\212\217\317\344\200\242\307\262\357v\270\023\213\031\241\031\314.o\343\350\235Rf\016\323rPd\370\367{\327\273\325\365T\204+\012\215\001\350\201|\234}b\314\345\030,\012\247E\016sC\317j\257\316~\333\203\3148(\361&U\264wtj\203\321\246\017\265\244\220G+\231]]cF\216\270\333\355\266!s\300M:\032u\225\013N\005\306t6$\236s\347\222AY\313D(L2\352R\307\235;\340\314J\310O\210z\327A\221\224\327\216\325\317u:\021\357\3740D6\227\240\321\353\332\365\236\034\374\232\360\320\001\370\256\035\241b\267\344\366W3\010\036\251\015\220\036\306ix6$xc\026\346\002\351\255\034\205;\000\361\206\014o\2256\020\360\035\312\013\236v\221'&\343i\312X\307I\304\244\311\012l\015\323\254\030v\304+9\223\374m80\012ZY\234\235\256e\010\304\223\2308\270\021\243\032p2\322\245\222c6\216\275\325UM\325\015\016\221\344z\225\300.\321\276\264\032\2109\356A\262\234hU+~\374n\327\2507~Bk\227\310\247\3103\303\241\011\263K\247)\004\015\011\216\326?E\326A[\010\375\305\027W\337\326~\033\333\3323\363u\376\241\341\363%\347\027\3559\003\340,L\336\040\311\015\324\265\367\370\372\346\322\353\337N\016\367C\365\347\317\311R\023\177\216\306\274\236\025E\310am\214\374\026\231\366\014\3520:\306-D\324\346B\346\226N\040\311\031\332\343\021}!2D'\241\234]^f\0030H5\264N\214x\040\241\021\360\262\322\"4\312\211f\330\021\376q+D\325\345\254\017\015\356\005qf@\012\343\015\217]X\270k%\025\372\363\317\177\211v\266\243W\214CaW\373\371_\317\376w\364\013/\356\351\245\233K\213\263F\300\345\034\237\013~\347\370\234q\015\347\"`(&l+f\323\312\3171\020\203t?V\211\265x\361\372\200\267e\177\261\266/F9]Y\224\223\227\030\374Pc`p\356\315\352\260V\214\223xE\355\341/5hl\304\203\375\005\033\357>?=\247\306\360\027k\274\013\211\242\030\357q1\001\373de\355\211\312Y\011\305\330\220u\264\222\2109\255\037\331$\224\231\251\2401\344\032\376\300\335\332\232q\235\013\365\270\243i\\\356\305\032A\247\265\014\017\357X\360:\256Q\016\362\034\3639\040\203\326\331\234\224\305E\025I\006\241\212pB\276'{\202dA;\214\332\027^\322-\313~9e\015\031-\301\202W\3518PK\270W\252s\355\257H\256\265\306\013>\373\314\340#6\361\262\250\333\211\226\363\321\360\214\3772^\354\220`B#~\370\0228\357\246\031`\322-\223\276\340D\266#}\267mZ\265\004Bg\273\375\223c\224`\254=\345\332\036\260\177\266\204G\250\266\277\236\360\"E\225\332\233\321\352j\346\350\346\004\352Z\343w\331{\266\003\301\2454\221#\337\245\254\231\354S\373\273\337\275\367\341mb\276\257<\256\230P,$kxT\203kgZ\244\214h\315Ja\241\260\266\026]\347\323\364r\006\367\312$\277\310\207\267\310\003\337\024yEn\375\344\270\234\342\013\013X\026\207\234\275\036\2606~'\260\272\005\262\327F\372\377\326>oI\247|z\272\030\015\351\3649\373B\236\003m\363\207zh\302\235-\300Sy\017W\240\2519\011\215\246\033o\345;\321\015Q\236Z\350\2441P\215\360B\010\264\320\350\323;\235\006\211N\303\014\\\231\326\306\264\231w\034\026\326~5S\\\252\310n\206\036\325\247\026\2654\247W\351\204\342\325\225\266q\314(\0333\011\334\260\003I\003v\311\207/\005\024p^\374\2004\305)\3436\207:S*\253H\211\246\327\343\356\032T\315"_buf, - "\012\330(\237\025\360\011\346N\307E\202*\005\247,K\336L\040\270\311\360yRB\026{_\261f\016\211\034A2\370\347,+Rg\004\232u\225\2347nv\225\317\212Az\304\226L7\341\032\211\337\355hm'rfd\331\015\376\347\352V\273Ad,\024Tg!/\0125G\376\375m\354\035kcX\333F&..\373\003\3024\344\347\346\006(\203\315a\207\360\323\\\333\254\267\200@O5\370\250L\211!tD&\307\257\2072(_3\004\202I\030\265J\352\251\266\016\005\317#\015\340\343)\366\204\033\2537\2369\254\267\236i4Rs\020\341\301\212G\231>\3464\250\335\001T\361\327\331\005\255\277\374\031\016\247f\265qK\233\216\306\301\262n@\224C\335kE'>\366!!|\012\221n\346\246\263<\0225E\006w\315\362\215\362\203\352\337\024=\040\203\003\231t>\3326,\300\334\246\257\253bG\335\351\252\335\230\343\232\026\232\273\243\2679\206\015\344xx\364;\262f\221\342\263\003\233,\326b\252~\231!(e;g\002\2148,xi&\303\341\021G2\366\343\206N\340TC7\307\021\255%\350`k9\262\266'R\246\323f\251v\324\214Fk?=\326~&~\223\374\025l\013\006\036\214,\366\330uSz\343\226\201\247u\324\016\202\241\021\034N\0307\234\015w\213\253\031\310\366\245\307\367.)\256\274P\270\016\204\032\244\325\226\314\257\274\003#:\233Mq\244CQ\234\245h8\022\366<=8\306w\335\347\007\307{\277\276\332=\375\353\341\361\313\266\211\262L\256\313\246k\232\000\212>WA\224f=\266\222&\21038\206\000!z.\212\364\233\366\302(\234\017\356`2\004\025\334s\015|Un\3558\340\260\274\001\274\027\030\210\3073<\275\251\342\373\346M\244\265\366\025\3338\247\263\211\232\203s*\320g\240\322\212\202\370B\245\227E>\233*P\370S\007t%\013j\301\300F6\021\202\022\033\243z8e\212\022\201\002sF\005:\224R+\012\002\"O\001\003\324\256(\322\201%F\241\367\230\360(8\272\227\011\223+\207\031\273\001\330\331\347\0016\370\2336\023V1\221\247fT\006\361J\213t\3303\343\035\310~\371\276\333\325\360e\373\313\306n\316\256\343S\302a\235\311IT\220J\275l\3562r@\347jU\025\244\312(\234\277\2614Xb\243\231\300\264\322Zhl/k\260h\367\233\220dY\230h\226\037\262)4\277\307\006\345\321\"^1\326\"\013\321x))zct\033\334\210\227u\010p\016\342\322\021l\252\371\333\027\323\270\017Z\221=\036\330x\211\022%\200\3052OF\022-k\025T\212\222\250\015\266V\352Kl\302\2212\321\203\360\351X1\3209N\0014\305\307f\210\231\220m4\3450\255\244\360\217E\207\233Z\214_\216\277\015C\303\301\201\243c(\003\370k\224\271\327\323~x\257|r\304\202\274\304\360G,\177\213\361I\332\014i\231\370\237q\024\204\324\011DGp\\\242\372\234\306\017\323O\013\266)\367\362\331\244\322\026\317@\\\001S\035\304zoN\025\202W\306>\360\322C\321\244\226\275\236\371[\243\246$U\3129I\314\362\332\241\372]G_\021\235'?;\001\026\002\325\361\017eC\230\347y\005\252M\001\241\242\237j\026-Lc\007w}\342\364\376c\023\035\275\032u\022[\275)\011\303\352\241+\242\206\357'U\322\035%\377\272Uad\272\334\"\323\214H\303\244\204\376\203\201h\024\0334\3046\274\353\244\344c\003r\345*\216!\223\200\2640cb\\\256\256f\040\264]_\020`n\032BW6\206v\273\000[,\214B\307.\357Q\006\217\233\020\257\023\003\235\031\0208.\317!\336+\030\220\320\277.\332W\220\031\264\254^%\203\002=TcF\320\261wz\276\377\374\245\036\012\241\250\206\027W\201\334\332d\265\316z\330\347\035\364z\372/\323\316\203\311o<\036}\177\257\250\316\322j\377\342\352\005+\210y\247\375\323\203\327'\247\347\375\027G\273/5N\021\333|\201FT\353\350`\367\257\375\275_\017\366\376\332\337\177\021\256\266{tt\262\327\177u\360\312\250e\365\014\215\332\316W\332\343\220\236\022Q\353\277\335==\356H\300\257N\366\01707i\364\305,\303\004-!h/\262\221\027\032\000\352\237\235\357\037\234\236\232\006\310{\224\367\016m)\231\3642\2424|\030\320z\200L\032\230K\316\306\027\220N!'\3531\262\334\266\373\177\016\237v\241Y\274\266a\372\332\310\014+\334\251\247~%\245\201\302\322\222\267\376W\357\322\213P\217\224\256)n;\373\330\335\240\366\016\006y\315\367\352\206\037\314\354BiM\2004]i\001M\341\264\225\220G\021\225GV\022\260\315\2326\030\315\344d2\272\235\333\030\215`\316\223+\274}>;\317S\241\347\236\021\272\213\007\242K\260\035`\305\204P5\322jK\253\272#a\2256(\032\021^:\312\206\356.4\350\344j\321y\222\357\214RT<\301W\343-\255\342\016\326\024sp\015\332q\014\223@\035\355xzj\250R\345\373\302\332-\230q\350:D\274\351YU}\033\214\222\"\001F\346S\365b\224\337\270\20118\270:\263\230dt\225\027Yu=\326\013\263|\234L\262\3517\332\246\332\246\202zg\360T\250)\261\361\267HW\327\025%\272\354O\351\202\361\363uR\242\267w6\242\205\263\202\0369y\216Z\342\371\022\273\302\244\040e\357w\335y\331\343\000\354B\331\2050\375\037\223lD\211*\002\240\264xHh\2113\0068\351P\276\234\202\371\031\"\316\247\353*\255\030`\371\371\014\2370c9o\0359=\035m\203\005C\227U\272\350\331s;\267\025P*"_buf, - "\341\3550\025i\321\267\015(\335\254\3745\033\016\323\2117\277\262hObxR\334\236\353\231\032U\316c\225cyb\231\024\313l\242\360\017\005\031\022\210\330\312\033gIX\333\331x\022\233\370\222\"\267\313=ZAi;ag\367)\026\321\337?E\"\037I\313\315\210\302\367\330G\361\266\014\321\334\267#\371\324\334\353\375\232]]\327&\272'\221\232\216fE2B;x\373\324\362\020q\235\250%\211N+\222\361\240\003\263\345Dx\271_\217c\207t\316\357Z\274.\371\201z\274\376\027e[\036|\231\350\335jh\332f\372\332;\212\030\305\316\377p\202\256\217fu\325\223\360\011Sg\274e\227\275\207lv\242\225\037W\"\317\301q\366\316Jk\2052\010\332\204\227\276Y.x\366V\364'#\264@m:\276+Mi\363\374c\337\372\275\372\317\226\323\253\040\300\233\365\347\306\263\343\357j\366~\351\272\034pF\273\327\233\317Zk\313\273\272\212l\260\266?\005\323\314\010\024\305\231\264YrOl\015\325\267b\317\365\244\254\302]M8\022\006\275\215d_=\205\210\347}kw8d\227\257p\321\273@\323\337\322MT\266\272-\241\250t\014O\303\261L\371h\330\242l\302\377\310\000\237\245\351\001z/\254!\003\034\303g\345\235\265\217y\271h\347\375\270\362~\245f[\000\272\265\344\257F\200\371\267r\320\200\030N\346\370{\260\322\014x\220\211\306)\032'S]^\354\210]\274\003m\351\011,\020\023\375\337M\267\233\320l\267qr\305\203\017\0130\342=V\320\252\270\215\\J\040\243\223\226\017\012V\350\234\203c7\020\305w1o\214Q\273K\265\033|\320[\012\015azJ\363RF\277\267Z\354H\312\036\341\266fE\330\361\335\222\346\037\036\350Y\230\346z\223\321\321\247\266\211\313|\230\261h(\236\005=\213\020\334l\315wK\275\263\255\254u\311\203\001y\337\310\247\371\350\026\302\225\225\335\353\260\345\012wi\231$\223\030\201E\227\374\310j\037\270U\353\260\355>\205\232}\330\317\256y\237&\235\020\360\344\036\367\343Ta6\225\212U\204S\216J\320\363\035G\355&\241\307\347R\206l\327\245\305\334{\375zc\243\177~\322?;?5\034\023\371\342S\362G\011\265\302\020\331\272\242u\276\364\311%\317\312\27164\351QO\361\336\364Y\333\234\3119\317\314\003\266=\256\215\0026\262Qva\027A\306\027\263\310N\351Q\212\324\361\252\010V\004l\366\255\367n\371\264\355>\212\327R3~i1T\236=E\253\204\311GPQ\210\255\250\253Q)\360&\317\377\303c\203`=\312\375J\005\034\020H\201\207\230\215\"a\327\375\233\243\327\234\244@\214\301r6\236j\341\257\322\241\236\002\\\040\222Y\262\3238\035\017\246\267\361r\326\211\226/;\242\267\313\266\313\040d\006s\312\340\375\374\22360y\224\346\215\214*ZC\003P\017\031\032\242R7\264\241\034\332p\356\320<\247\364\305\353\035E;\222\3218/\253\203\177\316\222\321\233\321\264\214_\274\246\234\302\354\337\002\376\235\011\204\230\030\301*\354g\227\306z\363\364\011I\221\225\020\342\023R%\034'\307\302E#\031\335$\267%\006d\004kG7\312'$\226\307\270\240\354\354\315F\230G\026\364\311\027)\2233S`\337\321\266\023\275C@\213>\273\242\300tLn/\215\320\213\\J!B\310\360\307\320\314F!\033L;\020\336\335\211\252c\0322\214\270z\031w\006\300\3364\277\027\372\367\002\277\033\270\305\014\300V\264\216\351\257\343\202\376\366\274\027\274\316+&\247d\030{\217O\011z\270\254R\036\370\265u\037\352\220\346\230\355=##\262\215\377\214VM\304!H.J\300h\215\341\355\356\035=n\256X\371\235x&\326}k[\333\006F\006\024/)\177yt\362|\367\250\177|\360\267\363\335\027\347\220\243x\211\014\001\201lL\230\034\226\\\202\353?\375\376\324\341\037n\333np\225^OV\277\214Y\315[S\270\344\347U\201\344\005\254&\377k\016P\013\246r\367\376\307?\376\021\325\216\011j,-\005N\031;DL\016\231\306\370/&hd\177\015\263\202\274,\264\303\005\265\344s\014\276\335\310Oz\260+\250%\003\215-0\355\032\015\303\264\227\374d\250\341s\354$j\316\245*Z\342\026\232\323N\2738\315\255UT\2025y\315\3261/\040V\356?g\031\223\354(vl\012\261q/\251\217K\334\247l{\257\301\356\346[\257\270\312&\320\374\371\214\322\262\344G\023\314\033\234\306C6!\307\207\347\177\307\030\265"_buf, - "\222N\021\357M\340\024\365\022\233\006\311\037\377\273\320\376\246\372\026+\212(\256\362o\360(\213\310./G@\011\214\017D9\356\374{\2053\210\257s\274\355\361\255\364\206q\014\234'\226~\262\0308\331\250\014\026\246\002%\230\015\251H\307\220\237\331e6X\322\224e\313\225\2252M\004\203\221\026\354\330\023/\260eb\203\232\302\3760\273b\2376\326!\267i[\007\311\332\022\247/X\244d\222On\307\020\375M\362\017K\363\004#\031-\2233\032\230\334\221\242\260\030\310\374\025\262\275\366\360\330\374\245\257\313\303XK9\010\354\343\212\031&\223\360\362\237Mv/J)\357\330%b\375\331>e\027\217\275\354\022vo\334\247\032\237y\315\350\016\\\236\251\332g\261\340wN^\206\203\343\027'\247{\007\261\332\021\353\300#PT\020\336\214g>\347U@3\336]Y\262\025ul\012\361\363u\"b\274\262\031_\233\244W\011\244\243\350Z\351%\036x\3308\210\207\0368\311n\270\313@\222.\237l!\366\341\307\324\325{\210S\307+\220\330)'T\254\014\035@\361\313\256\304a\007\015\005\\\024\003\222\261.\035\003/\203\355\"\220\205{\275\240\351\233@\005\264X-LU<\257>\037\223\201/\341\010\014\233\265\235\265\"{?\313\033\205]\347e\307s\260.\340}\221\221\244\271{\035\000|F0\364\033\350\330g\331\274n\357cU\340X\234\336\331\330\325\241\265\376c\274\034u\312\356@/\305\232q\201`\207?k\264MG\244\326\353\"\207\340\376Cd\304\331RA\012<\236\244i\004\343\302\255\233p\376Cm[\375,\231\221\375\030m\232\\ah?\036\326\017\013\234\214$\300\035\343\207\255g\335?\361W\2462\312)4$\327\206\201i\310\204\311[<\002&O\301u\221\262\023\025\206\256\3453\231M\330\3350\270\006\211wm\220\017S\225\305\304:t\306nY\350\324q\234\370\362\031\241{\341\245\337\263\226X\324\363q\254\226\274\261\205s\276\023\353\214'/\022\350t\"\337Gq,\304~\324x\331\020R\264\301\032aE\023#q\350H\322\342\351\216\257\243\011\327\233\213G%\222r\017\037\252\267[^\007\354\205\267\236\212)\031\040n\306V\250\241nZK\036\336\203\315\227&\336\260_x%\351\324\017\356/~X\341\262b'\256D2gIE\265\244\340\005)\014L\031\211\030\242\002\326\240nC\270\272l\274H/W\346$\276\322\300[4\327\022\252\304\240\243\370]k\263\371\230\366\271\252\240fPLRP\373L\037\243\330\216k\342zU\333\336?\334V\307\266\221X\254\243\250\266#\357\374A\352\234\224DVx\035N\040\221\246`\006X\001\321\326\213\024\323\012\302\206x\225M^\276\355@\254\372\350\307\215?\377\345i-\266\365\024\300\363\365\033\316\324\302}\317\231\353\351IQ\261J,\206\242}\341\265c\321*|\237\363\032\310\316v\331T2D\220\015\325nQ\322\012\211\212[Q\264\316\220J)\225\000\2307\261-0)\025\337Z\327\013k\275\321\250\027\206\316Fm/w\316\365\250O\352B\267\243\226;\275\000\030\310hn\253Y\214\036\353\026\027\212\337\326\3577Y\346\241b\322\262\326\022W\335\273i\256\225\020v\304\004\277\311e,Qm\377\302\026\244\247P\237\307\030\353\363Tsup\206\326\253\214\007\205\265\246\336\344\026F-Ps\361\373\203s\233Hf!g\220y\273\250\211\335X_\357b\255\377\005$\005\370\240(\007\037\313\226\253\331Bs*\343@\335\031\317<\202d/-\211\277<<\265\270?\217^\007\271kWO\3121\250\001\033K\346B6\356h\365\303\267\011\212\336\315\360%\002\374oB\227.t/\266J\320R\222W\275(^\217\247&\320+4\371\306\366t\257\266\263\332\337a\032:\247o\215\014\213\266\351\264\274o\307\213w\346\223\215\370\225\336\353\011\242\336\246s\263\000V\346\326\241_\177\300d\350\335~\233\271\240\013:0\025\013=\007\007\336\203\215\227\336\320s0\306\347\316\006\364\032\254\023XN3\225-\303K\252\251\364\002\227\220\312&\373W\252[\236\021\3455\362\350\002]\0263\006\214)\374\366\345\217\025\332\013\361\222\017I@\211\242\017!\211\003\205\316\344\264\324a\372\354\306\262A\217[\004\000,\366\217t\213\271\263\035\313\2753\022\2325\376\262j\205\272/\322\253\364\323Nc[\014\313\"\347\254\032rc\030\315,\007L\315y1\350e\344\017\277\303JUt\260\305\031c/2\340IX\233\353<\003\007e\2554\253n5\223\234\0367\3071\276\307n\003\363B\307\370^q\224\014\377{VV\012\245\302\360[\372|\3475/2\306dC\360\015*\362\333\034i\226A&\256\333\333\366,\034\347>v\344\027e\212\217\275\370\352\364\"\203?h0\"\013\2333\310y\365)\376nC`\242\037\352\030\300R\237Wn\267B#\221(\307~q\222\306\240\331+\365zN\221w\225\040yzB\357H\332d(\263I`\024!\200\274\261\361\330\260\345\207\330\250\244o\266q_\002\217U?\206\015f\300/\3113\232\032&\261\306\221\327\224\311t/]\341\361\364'T\354J\354\224\367\224>F\212\212'\2775\266\2437`\004\266\027\207\332\016\242\012~Q\032\206\265\365ZH5\347\200\262\307U\017q!x\2011z\015\003\275\313\244m\010\324\243)\005\223\3613\236\267Y\331&\365\354\377V\212@Z\035\263\356g\255W2\2503\273\036;I\346%\215\303\000\216s\311\234\232\040\213j\212\346\333\365\353\242\341\007\266\307\011#\031\0229\253\340\27633\340`\346\316\215\203\300\303fG\364\0337\231\247\216g\226\242\020A\024\236\272\022Q\247\350\276sE^\300(N\316\235.\017\036\017\2330\335\005\371\333N\331\301dh\"j\025\334\373\320A\204\311F\223\345`\360\260\251J9\270o=Q\247\300.J\034\365_\206\365)r\225\015\371\272v\017\323o\260\006\\\313\223\177\004\355!\373\335&\314,6\317n\356\316\245\211\244f\011l\331\306\206\024R\322\362G$\274\303\306\210\022\333\031{\257v\317\220zo\202\\\243~\302\273\230\376,U\336\262\343\375\211\335\376x]$\223\333\233\344\326\361Lo\302E\321\264\0017\345s\332$\034\277\230Hf\203\244L\353\002`pm\333\2251\270\230\257C\207\200\372\357.U\235D\013\245Fc\205\355\271~\327\346\332\314aml\011i\356S/m\033V+^hf\377\236\226m\340J\221)\225,\351\350\266\025\365D\251\306\252\262rw\234\226\220+\005#\036%\266\222\254\266q\301\362\353\366\301\222\221+U\206z\214\365/\226p\306\372s\016\227\324\366Y\303\260\356By7~\337\24187\376\267\031\214E|%1\376\316\353b\3371\337f0\316\265\253]\304\337w@\036.\343\236C\262\307\244S\015\256\216\360\345\377Y\354\336\251\035\213q\265q\2606\200P\262\271\3738Bh\332\031[\201C\236\030^\337\207\331d\220\314\256\256+\345GQ\206\003\300\341\335\350i\201\021\013\233\031\350\373\373\323\214\353\311\272\3330\256\207\004\223\341XpZ\232\201^O\373\021K\311\036\302\304\212(\271c\021U~\376S\215?-a_\204j\231\017\200\002\312"_buf, - "\277M\312^\357\344\022\355r\350uZ\313\336!c\334k\230\351\212\000\321Y\254\3725\"\364Bp{\001U\213O\015\011\247'\003\366iu\365j\224_@VM75\004\256\2461{$'\347\305\366v\254\177\220\352\016x\327\211\202\026X\242W\270&\261jW\224\270\017~\336n\267\036\324\353V]\247\302\332}\234\334F\223\024u\2539D\323D\333vHKT\244\311\020~\224\224\312\013X\261d8L\207\024\370p\002\271FY+0\3674P\327g\027\235\304yo\337\356?#\370\250\231B\301\376\035\273\373}\201\355>w\3477\337\370\265g@;\002\334\300\015\362\036\304\012S\325Q\304\215\235\356\276\327\264\236\015\330\336\033\362Yd\327\217\3763\266\023V\210\364f\374\2478\305\034\177'\217\003\232\2442Id\030\333\311C\032\347\217\320\234\250\355\304\246`\334f\241k&\220\270k2B\343\347\3622\346J\266\307\305\312\032\214\211j\3417#\367\271\037\215\257&\036f\252\353(\216\036y/\031\260)}$:i\233\214\276o\212\362\251\331\015\215\"\224`\2037\004\255\020\377K?O\332\366\234s:<[\276\220\231H:\032H\274\317\014\206\010e*\366}<\306Y|\267\374>\212ET,2\363\347\277\330}\352\2301]g\2434\302\364\366\345;\254\375\036\010\361Jg\005\2147\263\022oN\303\222FR5\310'\274c\264l\267}\202\342\352*\367\040\010K\205\006\026\014\313{\341\000\355\374\030\254\255\261ou\375\363K\001\341t\313\331\005\034$>u\300\203\254q\336g5\332\320\267\201\345N\004\211\317\376\351\237\177\314\213\375\317\031\273;<\221\224x\270\257\014Eb\336\015\271\215p|H\005\356\315S/\266>\315@\206\363F\375x\251\255\351hV\007d\345\367\337W\002\3015V\263\272\251\364\332O\266\366v_\237\2779=\210\246I\201^\337\251\212\312\317\256I\036o\2120o\231S\354\304y\343\376,\353N\010g\306\356m\341~\200\207\217\211\031\232\031\247\231C\230\346\0245\021\3770'\031\346\230\225\332\263\214\313\007\316b4C\254\306{s\012\204\235\355\300i\011\"\374\312\273\225\236\247\364\263\2674\266J!Y*\362\025\027I\231\015\320\311L\032A\243\301\3360+\223\361Ev5\003\317\224\213\264\272I\323\211\015A\263\266\002[\035\232\303\374R\371\030&\305UI\206\314\031\246\206\200X\033\300\201;\350mY\350!\021\347\323\215\267K<\360\204\330\302\030\352\233\236\321\276\367\316\301\235\267\264\315J\015\224\250x\247\026\243|\032/\202O\313\333\363\357+\236>h\033\341\241\217\331\337\354\220/\322Q\307\003\021\316\040-\315\243m\004\277\274\254F\3421\0020\237\275D\022\256.\256\351\040\245\237]\0062\275\333\330\305Vg&Jv^X\007\021}\317|u`\267\315\\\026\356\374`\302\263\317\365\361<\330\360\365\210\036\316\364\302\213\240\2109[7Z\314\262f{\364\243\000\314U>k;\372\2415Qg\233\364\214r\260\241/\346\015\027\307\262j\006.\2264=\310\017\355\255\256\362I\033\244\25461O\360k\224_\231\032\2424\231\3003\\RA\014\000\324\025\341C\027!\034=\325\200\262\362\015\370\370!M\247\240P\232\246\205\000\225\027\240\"`\214\031\2722\224\241i\006\304\037>\315l@\365\025\330\030\033\254\003\244\264\273\357:\370\217\230\010\3005o\263\227\261\333Rd\200\207\230U\243\264J\303#,\205!V^l?\014\220\333z\371\033``\302\250\351\3350\040-\344\347=\3649\017}d\213_w\326eh\266i\221}\004\313P\337\233\252N\313\250\277\315\006\325\202\333\016\251\207^\225\2126\203\333\316\256|\040\040\303\366\012\006Y8>x\333\347\342h[R\203\267\354\316\314o\312\025\3640\035\245cF\006\310rN8ZW\343\351%\350(\340\255\035\362\221\203\360\025g\025\250]\3709eP\300}\212\315\036\310S\021\325\236\224\031\273j\312\333\222IZ\214\341\001u]\207Q\211Y\311\326\342\237\263\014\331\011\266\3020\305C\001\005\246\2345\206|\2722\0109\243\341\305-8\234\344@^\204\257\0268d1\304\330\375\025\367\231\320\312\250\223\200\301\321\200\266\210H>+\312t\3641-)\221UW;}\347\214\237\205\024Z\201C'>\307\262^\370d\210*\372\276^\240\231\254\273\2740\364\300a\221\220\365\275\256\027\232\373\0252\206=\006\235\240\335F\347\376\2560\376$\204|Q\031\230\374\247\204\300\215\373\270\002\333\321d6\032M\205\211\231\341\032\372\352l\257\377\233\036\356\003\025\016\343>c\001.\323\342\335Q\237\026\371=\003\3629Z\027\212'=^\207AOO\220\347\022\247%\260\256f\245\330j\023^,\263\242\276\026\367\004a\265[~P\257\201}`\365h\020'\366\203Q\256\3760-mr\311~\260\243\216_\364\375c\001\013\356\001x\365\321\231B\350\206!\267\266\261\031\256\002L\206YE\236\037\320\362\003\010\370\021\372\314\232\233\237-2L\020\366\323\262\252\253\302\240\250*HK\271Or\370\261V\305\335i\304\251\033\022\206OD\010\363\361\2360j\227n\0305Od5\030\030\012\261;M/\207\372s\252\213\026\335\353\035\241k\353\017gS\344\023\341\017\362\215\025B\014|\201R\273\360\251\247.\320\214I\036\365\351_~\336E\\\037\325\365l\222\261qa\367lNE\327\012\032-\234X\277\200\240c_\376`\204\360\215\305\012\365tmII\261\005\313H\213\355JJ\261\017\270\336\202\213K\261\323Q\267\0302r*\003\254\352\317j\336\212\276\336\025\014\343\225\312\235?\217\354\324\250G\216}\015|\340\210\314\365!A\210{\010@\346i#\360s\207$\001\210\\\216\031\007\270\233\011r\351.\3340\337\256\322\337\022\220\237\303\330\373X}co\232=\223\273\201\315\223\333/\3156R\236Vl\025,/\336\206\034z\230\002\213\273\017rI\012\226\316"_buf, - "\322`\022\343\324\007\377lb\246\034\005:\365|\372\346\370\374\360\325\201|\256\302\040\210\340\201\016\316\314\011\276\223\020If\024\264\345\215\011\000\275]\202\346\230u\266L<_G\262p\235\250u\263\332j{\337\2258\223\267\261\276\376\336M\040\005$\025\262X3\240\002\022\373=\311\275\357\000s\206R\025\311\244\304\327\036\004\201\341\237\"\332\230\255\366\374'\275\040\\\0304J\032r\226z\321\012w\370&\244\321\321{E\304\220\340\261\262\370z\270\252Yu\317\315Y`\311Y\353\022\232\231?(\212\037Q\255\005\227]\212qrL\335\2267\302\213\026\255Ea\373\325\213.rG\317\367{\321[\320\022\301\313]\016\011\321\331\031\303%.#\220\314~Y2\375\253.\031\301+\301\006\001\007\261\271dE\355\030'\223\031\276\011:\002\337D\310\264\354\317\321m\007\012\216\262\311\354\223\001!\203\340`\243\021\204\"\003\272I|\363p\251^8\021\226\330h//\317\225\025?\020\377!\001H\315\213\224\254\\{,\032`8\007\230\016C\211_\265\021o\314`\005\366Y\003Y\352\316\342\202\213\364\0062\0069\223\315\315\023hA\030\002\3520\362h\256|\016:|\030\316\351\024\221\022\250^M\272-O\260\003W~\351\365\036.\317D=/;\002Mc\306>\306\033m\343\3161E\024\254\361\324\254\241D\213X\303\300\256\302E\213\330@E\315\0250\2561\261\274\261.\357t\345\316as\254\333`8\015\270\004d4xZ?\231\216@W\227{\206\366\300hV^k\306\005\370\233\017\333L5~\226\201\215\341M*\015\004\331\231)\212\014m\006\331Y\005\253\177\272\204;P\011\341D\300\020\3510\322\217H_\363\331\325\265\246\212\316\300\351\233\366\023W\035\351\370\"O\025\302\327`\245\346\017\212T\205\346\224\333{\306\263*\366\246\241u\360\356\030b\027\314\345\326u,\276-$\333h+n\2671\002\302\206X\214@\222\201o##\316\360\251K\312|\362\347S\375\267!\343\351\002[\255\230\354y\220S\321\342-g{\036\262\334'\000\002\362\217\274\343|}r\364wF\301\217\372\207g\307\273\307\355P8{\217m-\005\253\301J\206\263\200/\346}m\363\241l\256\230\0026\346\027y\301n\351\03382p\271\221\036\024^a@_\232\216/\222b\220\260}\221/\210r\377\2018\367]\244\347\350M\002\261\376=K\313x\267a>\356Of\343\213\264\240\260\013\344\375\353O=`\206\200\367oP\021\206\356&)\300\334!\206Ge\267\024\314\213\040\370\336O\033?\375\334\206q\340\363\326\0250\033\024\274\221m\347\202\223\263\250\310+\366A\347\004\004\035L.\361ka\353\327\253\353d\362\001\303\241\376W~=A/\255kE\375DxH\016\270\317\010\312u\025\313\322\217\311\250\243\352\240E\231\303b\243\304'\353\214\223\362\003\273\371\237mX69h\211\275\274\215\337\275\336g1\353+\332\331\021\235|\241\002F;\3435\336\030\333\232\361\302\232N|>m\253\275\302-_q\236^\017\256\236=\355\365\264\0371\031\002\220\355~\231\246\303\276\301\004\245`+\202\245n\016W\003$\326\254\205\005\304\025V\3310\265\213\037\3039k\333D\031\352A\236Q\200\021\254\034\304\205m\261\001dQWA\265?dS+V\375[x@\000\356\\\356\037:\361\360\310:\233\300S\355I\314\356\263h\322\246`\333\035\306kQ\025\035\012\217n_\202\217\374\200\202\207B\332v\240\037`\226\3255\355\005\025>d\235\002\326\202\200\032\330\24586\202\272\011\017\014q'\226\303\017\231\237\030\223\240\257\205\361A\250\272\343vl\315\311\264H\247\020c\013N\036\335\016d7\262\024\330\374\237\230\220q\235]Vh%l\007\214\307(\254q\034\213\345d{}\343/\263v\364\017\261\300m(z\372\347\231\036\257S\005P\343\375o\233'U\365\330\2114\300\177\372\217\231\316\012\260\241$\303\217\011pJX\305\263\005\305_\217\243\237\237\375\374\323\306\263\237\237>}\366\227\237~\376\363\177<[_\377\323\233\243\243h5*\373\354\272s\303\361\021b\256+\211\346\264\242M\2674\313\206\360\341\236r\014\262\354P\177V\273+q\305H\342\342g\270\337G\337\270\337G\336~\255\270=u\267\210}\341\360\254\207\375\353\331\205\327\262\007\022\275\242\253\253\252\012xw\257u\306\203r\332\032j\370\324\360\322\362fX\311\362\362\377\343\356]\333\333F\216D\341\317\253_\0013\317J\240L\321\222'\223\315R\026\347\310\262<\243\263\266\354\265\344\314\354c\373e\040\022\2240&\001\006\000-+\036\315o?]U}\277\200\240,'y7{\316X\004\032\325\325\325\325\325\325\325uY\345\226#J\313\356\363\243b\271\034\327\321\011\025\206\333\327\316\217NQ\335\212i\341\274\250\333\211\257lq/\360\035o\264\314+,\263\253\325\306\335\337\320\025\205\363\253\262\270\206\355.\2268\332\025v{\376\236#\2752\020\372\"\002\200&0\241\252\312\015\035(\021\234\346L\310\215\323\323\342\331r1\303\364OrLq3\015\246\313\034\215\342j\354\376\346F\261\342F\210bH\325\235\306\024\306\2007\015\024Jn&\222\3423Q\0004\032\360[\324\350D\300\222\257\374\227\254\237\262\262^2=\341w\035\214\247\242\232\336\026'\207\226\024\021/\314\001Qwe\015i\207\002\2254\203\363\243\250}[\330\002\304\012\002:\360\033\356\310\335\242\327#\311^\232\216AE\040\2437\313\034\021\030\014Nr\260\242\275B\177\251\371hL\265\"\001/zt`4}\226\262\311\244T>\370\332\005\034\302\205\026\373s\027#\243\342\353h\231\203\260\232h\001}f\342\316\242\032]`J\245\223<\303\033_:\365\303\257}~\240\031\247\340s\264\254\037\341\371\237\327\007\340\245G\263\212\037\361o\357?R\320f\366\223\374S\3611-\017+1h\213\353\371{m3\002\206\215\267\347\270!\250\317\244\332\347:\224x:\2119\024\007\006\343\246\274\240{[}\255\300\032\311\020H\200\235o\367]#b\372\031K\037\034\301X)z\326\011|D:\274*\377\233-\304l\232\201{m}ULD\325\355\373\247}\333\372d\301\275\326\331\265\251\264\246lgn\307\363d\261\332\337\325\254z\254M\277\363\352\213\351\224\252\273\223\331\245\223mi\200U\232\351\002\012g\200\332\277f\007D\262d{\023\006\344\350g\317\245\016\266m#x$\303\010\241*zk\352C\264\341e\243\265\316xEa\335U\326\200\376\002\376\233\003\364v@T)\024\247B\265Yn:(\266E\217\225\366\235|\026\267\025\307Z\367L\006\213n}\275\314\261\326*\376-\027\233]\270\320\307\210\266\002\231\\\216\3302K*\203YC\215LF^\345\273\315\025\302\363\344\362\020>\326\244\225x\344\313\342\000\2759\201\2322\202\336\331lE\241\304D+\260j},\276UB\251\355\212\327\306\275\326\372\025\343\363)-\366+\277\322\362\273\335\316X\2756\035\211v\333L\331\313'\336\005\205\243\210V(\035RB/\330\011\005\341\246\376\214.\313\234\332\220\006\216\212b\0304\256\313d\322\200\2277\260\303\307\005V\270n\324\354\371\307\346I\217\343\351Ij\015)\345\024\222\365\356<\321\260\250\360\357\345B\305\353\004v\002\367`&\277X\305ag\324\205,\307\252\261S\330\230"_buf, - "\374\354\344\354\360\351\213\343\321\361/G\307\257\317O^\235\236u\375\032\263\2302\263\202.\324\027WUt\371C]5\360)\225.\010]\231=\326#\232\210\203Lp\215\012\253\017\366\\\213\2222\257C\333\317q\343\374\331s\315Xk\226\326\230\013%`\006\326\017\307g\242\271\256\273\211\003\212zk\207y\210\031Q\0154P\333\221DBl\210\330~7\215\024z#\367\030\240d\252G\3577\367f\017;+\347;\353\343&\016u\0150\012\011\373\325ZH\324\326\307\215H\310-\344\234\273V\025>\2424\264Z\013\2654\014\247\231T\266\332\247\221\312\325\364\326\040\225\365q\023\022A\235A\341\022jr7\032\205\220\362\256\007w\251\031_}\213\303c\323h\334\245\320w{\247\256<\207Q\227\370_q<\275#\232\252\223\365\021\014\233\236\300x\227\010_?E\265\034\216-\035b\245\374\353\330\254\177\000\011L\215\201\342\351\013\035\037x7E\270\327\371L\206\216\2576\357\375(\034\350*#\020\011\315\355\362\035\333\300\307\34001\220gZ|\202\311\336\371\273\247\344\024\346|b\252\336\36254\007\2719W\036|\332\325\244\321\224C\212=}\203S\303a>yQ\214\311\023[\333,\264\307\275\210\203\340\032\017\333S\353\317\014W\376t\0332z\2462G\270^'O\033[\354BE(\336o\277\230G\230\337\235\261t\255\301r\353\260\335\216\235r\307\220b\005\034\361|C\270+E\3746y\334u\257\330x&p\027\364\304Ff\010\012!\374a\341N\006:\2010?b\212\351?\000D\373\346C_6K\266\006\336\342\231\261Z\244\343l\312h1\316\312\361rN\266{8t_\245\221d\025p\340\277N\362\332\007\006BB\211h\230\311|V\025\370-GA\214\241\037\235L\311\373Z\363\256\326\241\\\221;uO\317U\223\240\2120+\212\217\024,Pb<\023\207\354\205B\235m\261m\364*\233MX\263\036&7\200;\2114\231\010Y\243\341\327\000\245\357{wH\2718\257!\356\222g>\275J\307\037a\350\220b\265d\344\303\314\253i\377\322\373\275t\024\007I\250e0\374>\230\252WK_\012\337\300y\354\307\343\323\3437\207\347\307\361\236\012T\363|r\353y\341k\0135\237\040\2444\253i\332\307\005\223i\311E\001%\267Q\343\254\242\357\243\234\261~:Q,Q\365\275\271HM\316\353[\353\040\306\355\334z\030JOJ)\252\031\2115\3366\241\223,`g\033\320\301\217`\306c\033\266?'(O\272\250\001o\335pg\230U\256x\011|^K\314E\276a\330\262\027P*\231\351_\270u;\253~\005V\2742+\246\332t\305\221\220\013\250\"\003A\202\264S$\363\010\253\340\224\360d\225\006\350f\322\031m\375\304\213\356\223z+qsJ\3336t\011\265\344G$\242}]\371w\250MK$\007Fg\315G2\021\323Q7\340\276\341Ytq\364\240V\344\205\232\361\020\221\035\207gQ\266\206(\374\270]7\342FTnI\015\355!f]m\344Q&\256Y\215F\\\215\367p\203}s#\2456d\372\367\225\006\273J\024\030=a\007\305N\254\264I>x`(E+\214\313\024[\337h\353\324\006\217\021\263U\032\330\201\331\226\230h\233,\344\225L\210\1771O\034\006'\316f\3055\223\273\0277\220x\201\022\313t}\260`\233\313\213<\205\354sY\2556@\332LQ\337\205\210\032&\337s\330b\347\354\370\304\266\364\314\273\001.\231\202?\213\300V\204N\305\\Yfd]\316\261\212\011\003\217AC\225w\213\303\334\226c^\005m\214\361\016\264]\376U\354[\177\205\241\241y`\"\262\217CBM\037\260\277\236\035\037\201\361\372\257\376\276\266\267\317a\303\302-\030\266\325\371H\214\233\0225\263\376s\310\315p\001\207Q\254\346\260\275\355\0054)\260$K\021]c\264\027\346\340\326\021\0361(#c:\2320#\026\324\016B#\040\365\210aH\370Q\306\3778\264@\031>\247\205\232A\306\312yqQLn`2\000\020\014\325\373!\356\0126\015\032$\201~Pf\232[\352\227T\267!$\201\177\353h\226&\260^\363\224\263.\347\265(\375\234\216\2275\272\262M\212|+\300n\022m\012(g\273\322\210\211\264\346\272/\332\010/\322\313\214\311\260^\353\017\320\004\267\242\371\273\017\253\366V)\243\033\010k"_buf, - "SX\212]\266\316\317\210Fq`o\3206+<\355\332\370\177\365\204\266a;]p\364!\230Rd\250\345\032/\350\363\351M\3046\342|\024\202\307s\220\203p\302\014\226\220\325\200\342)\005\037\003\220y/\242?\304zm\004\027\305\223%\036\017(V\250\352\022|\011Z\260\232\204\036\324.\345I\024(L\177\356\207\272>\247\263KT%\323t@\0311\351\274s\235\334T\3462\340b:\004I\360N\226c\256\230\252\246#E\315\031\215\375\221\246\336oyJ\221\350\001\241\012\273\375\031u\245\251R\015\214!\007\273\031G\002\204P\241CZ\312m\243n\307!nn6r1\344\311\307\353\245\262`\2428\226C\3562\232}Jg\202^\235\250\273\277\021>\011\360\2235u\311\307\035\035\004;\326\315c&\2256\231\346\266-\314\010\373+;\344\\\006\025\004\364\256y\266\220\032\255\261\373\015\353I|\217R\034L\236u\231\361P86\200r\231\347\270\363\020\324*,\0379\234ud\272\252\215\324R\002\340\311\324\030=\211\364\201&\201\032;FL\261\245\227;W0\212\332\003\324p\333\011x}\014\202N-\004}{~!:t\327\251G\026\365y%\006\214\033\352\242$\337X\017\375\225\373\313\312\211\016\257\341\333\215\3657\217\333\330\267D\245\316',!ItAi\216\307\037oz\"\205\227nz\334\031\346x\023\352\003EZ0\344@\336I\247S6\023=\256>fB\007e\012\245\204\004\246\236\260%\007\265T\012\353g\333\303\244\300<]`\310\202\355\203b\301\320\342\241\365\026Y\316\021\322>\225i\275\342FPC\026,\330h`\375\302\312I\362\033\276^\204\322\354\267\221\004t\302\337~\363\317\040\246\225\\\346g\"T\\\234\360&g\313\3618\255\252\351\022\222y\205\227\226\207\356\015G|M\325\200{\3532\016\326cQ8E\307B\325[]a\305)T\344X\260\375\347E\341\310\353=\3519fnQ\320\261\205CJ\373\243_\245\367\356\364\271\271\251\361\307\012w'\325P\030\032\040/\232\376}\010\031-\375\230M\270\301\300c\367\026\246qq\323\265\314\271E{0P\177\307\356-\370HD\266{o\3327!+%\3752\362Q.s\274\222\343\0373nC\311gf\346\344]B\352%Z\270\334\323B\272G\330\315\341\212\233\377k\345\264\344\367\327z%v\216\224\321p\226@\200(h,L\250\003\202_T\020'\344\0055]\327\342N\247\267\333\355\231M\022\235\020%E\365\307\241\214\010:\336\374s\310\251\314\377<\220\374\024\010\345lQ\315\"\262KYl\370\016\032\322\317\370@\242\326\327B\023\220Ff6<\223\306XH\\0\222\000f~@~\243\222\020\354$X\313\234(\033\356\335\"\234\327\324\005\002\243\203q'\332G\025\207Q\332(\345B'C\355\313\276{\0160\252$4\250\335\032\224.\030\310O(\265\2018v)\231\012\241\000\374\000u^\040Fzf\301\302\332\203\375\0039\272\031\317\034C\255r0Ut\3304\361\034\014\304\235\261\015\267\305}\261\234\247>\371I\311\2372\200\330B\21013\3376\221\247\355\304\321\301j-\267\302z\360\300\031\022\236\3244UR\352\227\274\014\256\266\326\3642\031\264<&\351\254N\234\245\322\307\307\261ZFV\202]9\314\364\363\002\261=/\300\265\203:\324@\366e\222\227\376\0028k\022\015\243][s\325\333'rK\354O\031@U\273\314\327\330\006\276\263\323\246\255\013\367V[a\265\325\034R\234y\341\3547\256c%*\371\252f\222R\020\255\315\231V\353\262Ms\213\215\326\373\002\362t\266\370\302\024\331MB\311q\205\364\255B\331\310v\254\322\206nz\372)\035W\337\035\270\202\331\340P7\346\011\242tX\216R\274in9B5\015\202\335V\263\336\274\215K\216&\226\220:\037\251b\034\374&w\245\263S\231\323\323\276\254X\015\256vt\007\257;\353}\354\272\002\322YN\264D\354\345d)\327\257i\221\036X\006\011\355\316\372\001G\011]\006\273n\307~p\036sD\206\305\017L\336\301\215\300\334/\373\305G)[|\301\033u@n<]\326\257>\332\243\365\306m\204a\370e\204\347\366\271%\0257\254\344\206h\240\317\251\244\223\250\372\304\331\213\352L\306\327W)X\252#Q\346Q\030\337/RtJg\213\270\253\331gD\243k\260\266\314\023(B\205\365\"7\254kF\005\204m\023x!\000\260\320\324\316\013F1!\322\337\010g9\324%^\210\277I\346\021\267\364\264J\225J\2777\345H\003\273?0\330\375g\312\240\331\265f\200\303\307\012\231\2252}\350cg@\330x\257\213\022\315\364f\332\301\022\336\031\207L3\337%L0_\262\007|\261\272\276\320\206\004q\341\231>\316\366\241\266/\313\312B\304\206:\274\3567~$j|\036\177F\007K\264\251G\235/o\363\217yq\235Cr\015\3618\231\342u0\362\030N\336\004c\262n;\243\252t]N\365\261T\242\036\012\335yq\365I\205\2111\361U\251g\275\010\225}\360\004U+J\037\273\322O+C\021\203a\337\243f\246\241\304\2253\375\211O?C\371f\342\004r\016\235,\274>\377\226X\023\342L\250\262\232\367\372\246\011\326\334@\335Y\225\301r\007^\244\003\207\010}\236\360\010\254>\325;T\263\242k|\352\251\273%+9\246\327[\32179\232'\327\005U%\333\222\2661*\234\2742\334\020r\011\270\356\235\212\213(\340\311\347\227\033\266\260mj\376A\256\365-\310lk]\032\270\377[\315\252\341Z\320\006\325\272z\024e\264B0h,d&$ro\264\233W?\250n\317\213\222\337\001*\233UL\313<\264\312a1i;*r\031\311\362\3356k\211\227\361\020\007DH\233|\010u3\\4\272m\3019\307F\333\313\266\177\225TG\334x\336v\305\267\321\035\032_z\226\230\177G\341\253\231\266\3313\371\303\020\303)\3756\252\031\2200^\265\350\243\035\361q\037N}\207\23240<\276\3466\375\271\265\303\313!\032gX;\375\003GV\006j\335;\355\250\316\371\316\320\353\227\345J\340bA\002\270\353\325\303<\322\323\240/)1\2020\306&\247\306\326\223\224\233,)\341\344I~\206\021_\240\354\330D\011\224ow\356j\202\032M[\0369N\312\331\315\032\214BKd\311\326[\236UW\351\304\231\231v\3632E]}\177#\250s\267\237\321\306\331\324ZypV;\257\030\351\352\022T\307\247\030o\375\364\370\364\350\247\227\207o\376\353\344\364\307"_buf, - "\256\237\314\027i>\276\232'\345\307\327\230\215;`n\206\275)dS\365@\300\346\215:\245\374H\356\360\321S\361H\337E3\332\040V\364\254\200dZX\376\252\256iy\250~q\205<\2219\266`#\253V\367\315\241\360\326\355z~\216\242\323%4p3\324RZ\331)\007@\215\315r\015N\255\020\017/\004\226\034p\032.\321\311KZ\2571\377\327Xo|-\2338\312\305\257\270U\264k^\346l\035|m\227L\253`\213\315\254\352$_\012\237\015\343\003t\307\220P\335w+p\346\361\244o\363\312\300<\342\177<]fP(Y\360\321\005\377\351\305\236KD\016\221\207\207\312/\202\365\244tl\324\355%hg\334\301#hA2\255\024\2064\373\241\225\021\003\326\266\025,\245afIuB\3036\024m\333\370\277\220'\3220\352\233\220\243W\035]WM\221\010)\307\255\203[\306&\326\271U\277\303|\233\013\373\257i\343\011up\225\344\223Y\372\010\037?Lq\344A\212c\036{N\040=\255/\357\361\2236\201\322i@\\\362\326\331\324]s\016/\200\307\265\036\262\241I\177L\246}b\330\033x\371\200fA%3\362\224x\346\232\275\315r2\220u\203\327yb\011\243\316\274r\231k\031a5\023\211\274\332\243g\261\016W\236\322zFw}\256\013\204\225\370\206\303\005\303u\317\301\004\267i\033\025u-\241=45\333\335\0361Q\300M\300\320\231}\320\273\253\357J[\021\266\341\332J\273(l\270\3571\251\322x\345d6\375\027\270\3351\266\212o\362\001\315qw\277\341\"\314st\015{(\220\356\040.\373\367\314Jq_\341\020b\340\0312Xxl\361f\244\206J\265\340\266ly\231\343\001\333\356\022\340\341\303\360\345\314~kk\364\352\243\241C\025u}\027\326sB\306\214h\350\333#\320\201d\250\335\342c\027\207\323Zs\021\010\021\322\272\006\327\325\264\306j\312\221SN\331N\360\362\257#\275\333\230h-\360\332W\\\344\233\306\230\220\301V\271JP\265:n\2070\313R4\350n&\217z6s\250\234\032u\316\217\317\316GG\207g\307pg`\015Z\321\242\255\343\235\356\217\223\246\2237\214\034\333\232\003\233\022\371\331\0344=\370\257F\036<\255\235\277\371\037\313^DV\214\026>x\304DT\332\272\333\230E\356\370\227\327\307oN^\036\237\236\037\276\030\2759~v\362\346\370\350\334\275\213tjlG\245Su{\245\273\207\003\025\207M\016.>O^\362\201>4x;\016\246\2503\213\236Fe{\367\223\257F\307MZ\027\012\343\275c\027a\337_m5\020lH\2655K\026\025\252\226`\2463\254\203\302\020\200\377=<}u\032\341\306\304S\240\310\264[\233\266\245Rx\321\377\012\221\231\3634a\253\225\012dW\244m\242td\253M\004?\021\274P\277/^\270\340\337\272I\002\237\037\302\212|\365\362\365\311\213\343\036%\312\2475\255\262\222\363\334\202o\216\377\373-\343\\M\273\263\301CY9y5(r\012\210\254cx\203X\343\2100\200\033\302T\251\326k\337s\255\037\0204>\237u\355@\221\220\032?Y\316\3477\342\227\313\000\342x\040\2402\2413\203\302s*!Z\344?\215\004\363\247\365\314.\033\234\267\357`U\377\326\326t\217;\230cHm:Q}\225-\372\276\216\031b\211\372\354\345w?y4)\040~1\242\361'\036\251\345i\372'\244`I\007z\373)\226s\242\277\243*\273\314\031\017\004\335\243\300\177\234J\030ie\227}@\373\250\003\2562\370\207g\326\264\236\234Le\204\032\330\376\331Zd|9O\300u`v#DR\222k\031I!\034\263.\312\024\215\037\231Q\234\032\354\305T\271\036\003\256S\264z\324\302\205\204)!\327X\202\021\202rP`,s(\261\316z,\300G\321*\270\251\262\362y\015\371\245?~,\253\331\314\007\277A{\344\276\375\005\334=\342w\326\233\207\017\263\332\334\316\015\346\332\316j\203\007=\035\266\272\233\241\251\002?\215\330\265\246\330\346\373\236\341-\221W\031\323\2245\027\017Y\025c\241\271\3569\222\024t\345D\205\262\206\014\344\211\346\262c\335\010\230\322\212J\000\003\331\263\3529\234\304(\27724\356\227\266\236\247KQ\374\272\024\3363\200\265\362\361\241;a\016\332\270%\024\371\345\234m\203_\040\206\0021|\273\214spkR\030\002\212\011Q\014'\020\247\310\364\301\353E\2338\035b(-6\021O?M}\250\371\247\233\014\273G{\337Z\024\213%\354w\202\035b\311\016\221\247:m\300\213I\242\263\341\335\264\365\033\207\314\261~4e\024\354m\330\371\260\374\\\036m\233\\\356pcW\257x\340?\272d\206\363\203\317\256\012\226W=\237a/z\221\374\375F\341\022k\335\351\353\3026\207&\346\357/\264\230\021\274\036\372e5\353\317y\216;\300\245?3z\356\243\333\202C\033ZFa\263\252\335E\324J:\211[\231U\223m\013\251\365&\333\251\300\307\265\213\336F\243bx\217\342\254\211M\276\222O\310\346n\270\347\240Y\377^\254\355\016{y5\335\040\2675\263\206\314Fds'\371\372Z\356\267\253\245\313m\263\312\"\364w\251\210\237\0265\026\322\315\357\304~\353\260\013\241p\312\010\242\004-\246{\034\0144.~\226Mr\302H\242\330\2230[\352e\341C\312]\306\350q2\270\207\245\363UKA\243\027#U\252\223j\375\225\301\377\332\2777\361\272\232\341\333\260q\360\022\336\372\262\201\354V\006Yz\310mQ\317\322\213\345\345SLXt\240\305\231\332/u\235\305\202\200L\312>\326l\274\277\375\026\371.\345`\371\274r\017\320/^\364z\022\033\317\255\200.\033D\367\313|\234,\231&\247\325\375\364\206\246\205\311\245y\347\012\334\354\243\273\367\366"_buf, - "f\005D/0\203\233e\026\264|\002nK\224\327\021\343\313\320\243\211{\321\250\3500J\000\310\010\\\224\221H\352&\347G\246\326\207\303x\323\245\2625[k\026+\321\031\337\277&pR\333,\014\240\215\\\034'B\021\241\014=Q\040\040E\026\202^\251\210\217\250\202\210\\Y\230:E\235\027\345\315\312\032\300\334\3640\260\312M\342\036g\227\203\347\215u\245\365w\371,X\255\235\361\336\365O\351l!\370\311*\353<\313.N&L\310f\323\033\343\366\001\244s\262X`\350\373\234\355\0040\213T\253\040)/\307\275h|\225\224\302\232$\377e\257>\211\245\022\364\011\377\371\350\247\3037<\252\235^\217~>9\375\356\261\361\350\355\351\311\321\253g\307\335\326\370\\\003B\243\272\031%\27225\011\260\254R\332S\236\241V\244\37667R|\242\023\310-\300{\3040\340\205r\005\306l\013\212\025\216\330\300\302\020\360{\367\241\353\275r\257\254\012XU7\224\262i\317\022\252\3205\2769\202\274\345\007.\345\010!\350\274\273\357\364\254\177\352\304\026\251^9\354r\351$L\346X\251f\336H\021A\040\235\252lY\224\311`\360:)+\345\271<\236e\001\356\2057\376o\362\364\232?0\034@\304T\352\323\032;-\304[\361\306\251\272\306Q\327\013\341\006\206\300\016\237\263\314\207\201<\226\302\017;E\212\252\200@\037\014\255\230{u\333\3512\211\3457\274F\315l)\307,\001\307$\213+\340\262\242\232^O\302\002\216\301\370\013}\207\371\221\231\\e\307_y\216\341\325=x\013\215\373\371\223X\276\342\363I\365\256\340H\274o\267\335\2245i\016\326\371,v\264\321\321<\371\265(\371k\277\217\237\325>\313\327j\277\000\002\235.\347\027i\040=\204G\252\216.\230\"\316\276\202\020\317\026]\240s5ua\210+\243\025A\326G\273\337\330P\033fcCm|\246_\261\216U\006I>g7\040x'\040n\324\370\340\025d#\204<\026\033\015$Q_4b\243u\252a3-3`y\\g\005\325\250Q\014\024=y\"\322.\253wL\241\265\270\212/\210\310*\361n\265b[k\231\2247\202\335X\343[k\305\251\205\245\325\327a\275\263/\207\306Rcr;[\030\325uR\243\002OV\023\376m\252\355h\025\372\220L@\260\227\311\347\343\317Y\315%\372\343\357\277\327s\037y\353\312\221\237\370\033#\337\234\251\275\211\353\203\323\306\242r\366\316\207w\3272\026\341@\330\220}\265\375\334\232x}^\015\316\354\332\311\337\246N\336\307\247\317_\2759:\226\037\364\":|\363\356E\3355\356\274\203D\034D[\035`\023\275\017\370\335\331\352\330\316fr\033\324\223\232X\233\240\237\272Pd\301\242\255\273%4\322\021\366\361\365hg\026S\013\005\217j\343\262X@K<\365F\243\015\370\002yg\300\315{\371*\217\012\0148\002\305\012\366\213\252\307\373\242\344\022\214\014\313<\373\3332E2\020\262\032\341\2066\274kH\354\257e\000\001M\215\322\224\232\220\3349p@M\212\264\202\350\025\260\313B\316(\341\313\225\250\\\225lM\317f\375\350\247\342:\375\004\234D\225p\040p\305u\013\213\256)\343\006\244;f\232\212>\344>;P.+\243\346\017\244\327`\272Z\261\314'6\244\254\346\231e!\011\011x\211\300\025\230r)\003\005$\311\227\014\261\233\276o\231\325\"\331f35b\206c\344\320\333\232O\000\271\031\315\331a:\263n\344\234/7\207\3616\353\334\003@\311NQ\374o]\021`\262\261\331\203\362\216\261\272a\0070\331\243\217\343qT\220nMV`\024\315e\005Jy\331e\324\241\344\234\337mt\216\220\320\325ZZwi\005\025p\257\320\241\303\247\314:\250\015X\034:\015\313\311\347\005{\306xL~\320R$Y\341yB\205\375B\377\2304\350\251\254\232_D\236UC\012r\230\346W\036\257\024mv\031\333\237\313\244ch\312=\324\236\234Q(\336\266'\301\247\346\244\307kv\350\267H\")#c6\376\372\351\015\345\337\213\365\036{Q\023h\035\317,\377\224\314\262\311ay\031\354\210\341~\242ZySN\323\231\221c$S\242o\032\340\233\244\272w\201\240\253\355\300\240d\2671\323\371\003\370\242\237U?e\223\211\233\362\301\223(\2125W\225Q1\367\342\272\005\212\034\244\221\006\310p\234\032a$\004\002Y\2169\032\261}\237\036\251\210]\355\241\036\226\333:\2453\017\260\342\266?o\211\223\257f\007'C\012\365Y[\361\\n\012[7\341\352\334\312U\354D\0335r\013&\254\033\010\302\206\030M\345\"A$\264Dx\301\251\242\241@\362@\365\235\310c\272-\023\227\3723s{\215\277.-T\216\272~\3651[\254\004\355)\031\320\202\031\033Hb\260Z\363R\015b\236\027/\001\012\243\246\\\262\034\256\025D\343'p\037Ck\031\343\355\354\265-\177q\353\027F\017\276F\360\250o\007\372\202h\020'^b\320\237j\255,\347i^W\261\202\330nV=\313\205.\010\334\265\022\210\320\263s\323x\326\245\326\257cpR\351\231`R+Y\377-\255\237\2309|\267\207NY\317\266\006%3\373\264\032\263\331\202\372\347\313\333}\005\002j0x\311\267M\311\364\373z\266v\323\206\207\266I\266\177\242\021\265:\254\316\023&\321\2706\265\362NS)\2365\247\0136\325<\301>\245PnZRi\270\011\031\240|\212\200_\223\222\214iH\267\240l\243\002\215X_Ye,\206\337\236\015[?3O\371\370\365\317\304eF\037^\0064\010\360\0008c\377\271\002\253\037\207\201u\014\3217`TL\343\316\373\367\217:\276\240\037\266B\325\327\017\016tt\006\203|Q\004\005\225\354\207\022^\354\366\024\026\001\351\"\276x\267\373\201\341\271\365\207\255V\213N\016\360YQ\007\207\267\325\337j\030\034|\371\025C\343\040\332\311\010\230e-\355\210\312\300/\200\372\364\006\266\200\221\337\305\244\367\020L\203\2433\031\234(\355$\034\036\205YG\\\313\241\011\027\256\344\304\275\320\027+\351$\331\215\223\031;aLnN\320\257\240\316\222\332\227S\024\356f}\015m\352i\201\203~\217\206\250\363\012,~\220)@x2@\260\023_\344\342\022\014B\036\340\350\014\367\244`\034\204\342G\326n\343\304s\201R\317\213%\230\207Ca\2729\263\314\343\261\001\323\314\037z\216\361\027\252<\236\026\360\225\324\0217\264C\212\203\274\330QQ\035\363b\222\366\033b\035\237\235\234a\342\233\343_\216\216_Cu\274\263\256e\201#\361\222\352\026|\337a\327\036\212y\350=\326\356\316\315$m\321\003\005:X*\311\274\360\320\356\307\003\025)\374\025\022\254\254T\276\273\011\267.\317Q1+\226`\026\204\177~\\&\345$\346\317\3408\355\370\340s\256\0313=\205\215\003,o\230\245\243\212\2121f\237\203p\300\222g#\205\336\037\240\271n\353}nI\035\264\236\200\3314\305\273S8\343\0303\216\305#15\012c\306\322\335\024\254\311\203\315\225\355\014\032\004\237\220a\323\025\020=&\333\002\035\212A\264\004\250\263\242\003p\272\271,\312\033\001\035\200NK\266+\202\321\364n\040\345\347\002&M,\350\213w\003\310\257y88\037k83\264!n\231\345\014\335\321\031\303\365\364rd\262\327yq\317\3162#c\315\220]\241\3168S\220\304e7Z!\"\345T`\272\216\242_g\040\360\354~\366\235\240t\363\315\026\355-\326\036\023X\323\270\265\304L_d\273~\226/\226\365\040\300\002B\204\362|\333x\210\026\241?\2328\215\036\007\273b\200Wmt\354tF\027Q;?\340\036\204\002\300\277\310\255\351\324.\370\374\276\030SS\302\366\205,\264\320U\"r?\374\255&L\254\317-o#\343x\215\336\243z\200\256\206\376\356\352T\223\353\273\025\265^^\315\276Ez\254\034.\304\355\355e=\375\363!\274\204(\314k\376\364\035B\213>\350w\321\250G@G\031\346\025a\377<\301V\373\020A\353\006i\346p\251==\313\376\016\347\304\237\263I\212\036E\305K\270?xzS3\244\217^\217\336\236?\3773f\231B\327\242\354C/\332\331\353\211\332\020\370B\376\315\377p\212\214\012\374\3371\204>h\203x'\273\377`}\261\0362\022><\340\040\375h\205\334\206\032\235\233\370\264\311Y\350\256Ar\273\332I\312\324\303w\037L\212h\340\334\026n\252o\333\015\312T\217\314}x-o4\273\270\231xq\020\371<\213\374\213\314\267\325\240S\226\265qXK\034\35242\205\344\277\322\033\014\032\2106\243\237\315'\203\301\323\224\321\232\252/Q\305\346]\367\340\250k\027(\341\372\375\276^>2\205+\314G\202\220\027\010\221\364z\326\242I\354\271\005\026\260!\333W\200\217=\336\351\202\010\251r\203\260\034\274\356\203\032\040\203\357\227\030\200/\326\023\307}a\3140\347j\206\034\310=\023\211w\237\032\273\311\255tju=\361$[in|\336\2322\302O\316\247\237\266r\3643\327\003\036&\344[\035\264\356\006\250z\320\374\001\303%o\324\222\322A9`\234\345#\363\256Gv\316g\376X\024\275d\207\237\021Yk\205\245\326\177\364\321\343L\274\365x\354\025\255X\371>TB\272l\364\351\013X\260\321\257\014t\235\310|}s\267\304|(\033\225T\0301\203iQ\216S\376\010<%.\270\025\034\234\013\323\211e\010\347q2\212^\316\276\347h2S\323,\355\321\335\274\326\353\206.TZ\023\360\040`$\370\333\222\235+\034,^\341D\030Y\341\310Ca\202\305\007\040G\204\352\"\012\372\005\353+\2341\004Tm\346Pl\274\2243\000\031\322\277(\370\267!7\015q%G6\353\252//8\033\310\252\325\0358-\360.\201.\251\315\313'\270}\012\217\352\261K\323\323\242\346YH\213\022R\275}\270\236\360G\040\337X\224`\370WL\001&\323\004\213\327Y[\30234\3243U\312\267K\270\361c?\227p\037\365\334,&}\261\234N\323\022\014$\007Lu\030Z.\234\204\304\323\345\364\204\001cb\207\3347\345}7{\307\276\267\025h\260FA\230\376;\005\373\203)\3259&l\346\257\361/M\337\3629\210\032Xx\335\333\252\264^\304\330)\317\020\3600\202\361\025\323\030~u\243f\327\360\337\355\016<\274\352\305\205\021\341&\037\307\335\365\374l0\025\347\247\264\2342\275\221LE\343H\305W\372F\307;\361\270!\260C+\234\366\217_=\217\302~_q\264\270\240\\\211L\361M\027l\025{\303\237\225h\243Y\211\215\244\344\340p\244k\2000\315\303\030Qw\350\333\312-\256Z,\353q\034\204\271\352:\320\177\300\363\314.\020\230(\330Dc\235J\214\240\202H\001\232\006H\304\001\364B\371\242\304\252c+\222\015\223\367\261#\373m\240$1\270\204/'\361k\250\344\363\230zt\177\377\323K\267c0\036\2452\306\3548$\000t\313\266\324\234D\250T\334\215=1\333U]z'\005\247\343\274@\340G\254e1K\341\363\262!1\354\267\0365IP8\272\223\314P\342\223\313v[\243\240\233(\036\035\3067\032\306i\354\317\375F\361\250\272\320k\262H\217/o\274\001@\355\027P&S\371$\215ag\3643\225\031\315\004\326&\370\236\352\206\365\242\316\333\0341g:*\200D\210\"\214I\242A!LQc\240\302\357\332P\264\325\252\355\200\026\035Pw\242\017\\UGF\330U\002\"i\"\015R@j$\006\321\377)\214\003\331\275\357\3028\212oV\260\015#\335\031d\213\025\265q\370f>-\213y\004F\342h\271\330a?\340N\210\252\025\333_c\243yr\003g\027-\0159\331)\230.\213\001Hh\213\260\334q\304\300\330\214\014\020\321\3304N\367\313\011\303\204\304\257\305\040\306\307>\006\271g\016\3218\242\332\377\3073\001\312\263Wm\031\301\216\2642t\225'\216\370\035\016\3216\313\333\354\337\007O\231\370\306v\204N\244w\030\243!`\025\216\300\005=\237\374\252b\035\030x\2509J\315\027[\317\263\321\373W\340\240"_buf, - "[\353x\015n\227\203\001\371\340\210H\367{\346/\264&\300M\200\273k\360\024c\332\346\001E}9\267\221\003\270e\336W.\255\302\363\317g\300\201\251\246\343\312`\240\257a3w\225\001\020\235y\017\242\255\177\337\362d\023\2165\217\346\203\250\363\357\023\230\331N\330\244\253\367osA\213<>|\377\343\316\246os&\351\212\313<\203\010s\232\370\026\333\335m8M\227\007I}SW\263\261\357\313\015D\242\"\315\307\311\242\302\254eT\305\200q_\002\246\204$Z\200G.$\377'U\252\342\265.\320\234\015\006\203\013\260C\203\255\272\257\347N\040\246\020\2051\276\230\031$t\373\200&q\344\232\220\335hB\246\332\017\303\020w\020\220\253{i\324\325qAb\352X^\036\204\320\303\273\232\2670\033p\2179.\0267T*\376Q4\235\301\265\012\354i\226\273\021X\205\224\271\224\037\206\277x\256\025\010\237\240_k\244\0115\335\020\024\265\040M\254L\241\346\000\275:\230\312\316\304;\003\244\343\256\035F\323*/6ep\317'\351g\236\220\035\307hW\335U\343\343\015|\245y-\364\020\350\012\367r\251\353\227)\372\026\306\306$\020ZnQa>\354w\370\376\303\316\020\246y:\307\213*\213\035\"\272\274{\223b6|\0060\253\263d\306y\001\374n\013\314\240\205,\262\341\037\250\234G\354\3155\225q\341\375\206-\033\330'\365\265\002\016_\356\323\330L\340\306\2002\261+\314\246O\214\2656D\023\032wLc\332\020\261\252\221\351\255\250\252\326\237+\312\361\216?\030\233\245\330$\375C\371\335?\026\3476B;\352\032\254\274\215\327\313\014\335\250\2733\204\343E\247c\370e\2617P\374\302*^\321rd\222}\370\300,\371H%\357\275\303\242\203\016mvR\006\3209\3231O\2569@[F\337\343\336\375\207l\312\206953\231\236\276:;\177\366\352\35597#\337\024\313\350\017dL\247\014\001\360`\016\365m2\310\370\012\341i\260;0\2510]\346cUT\306\324f\270\223\310\027E\203\011)\347B\203\261\232\323\325\233\325\234=\0144\237\025\227ns\366p_]\275\335\332\026^i\263u\255\271l2F\230\301\304\266\351&\263\313\202\251\221W\206q\326c\325\035\363t\336\372\2431Zi\276\362\252\016m\241u\361\002.\210\217\256b\362\243\015\270)x\014ad\261/\360~\331\264\224\311t4\324rl\231\216n\235\272rH\262\352gF\014\237Y%\252z\276\247\013&X\263\317\221\307_E\354>Pf\216Z\211\007\233\233\374\262\345o\313d\026\363w2\326\231\377\346\225\211e\020\264q\201\326\022_$f\030\303\007F\214z\305\265Ij\357\364\306\360Y\2276\325r\332\2026\324\312O\033\376N\226K\021\215KI\2352H\236U\010#q\302(\332\304Q\312\004R\211>t:\205x\315$\313\2535\250\224\345A\"A\230],\033\370\242\350\234[U\276\222N\362\327\030\315\257\177\301\020\210\272\266\226\212\351\240!\353I\034i\361\366\225\303~=\265F\315\273\\}D\274\211\177\360n\337\274\301l\014\256WZ\001Nk\010\354\275\353q5\033\007\221(\263\271\037\003\3130\312/\367\225\347\376vt}\305N\324(\241\300\223\026nz\301+\274|_G\235}\037\356\232\265\232K\\\230\243\222\342#\247P\333y\224\027\030$\351\200\356\256\004\230b-\040\011\016\303-\033\240y6\345\322\037z\031\375\200P\253\345\005\354\306\324\260\027\355=d\035\356T\334Y\264\251\224\267:\013#\255\325O&9\272Nr+R\244\253\3215\220\363\335\207f\011?\306\363,\373\277\337~\343\177\277\257\365\037\271\376\243\3342b\232\325M=*\311p\001x;\220\250-n\032\357zP\024\333\2155k\234i\026\263e\231\314\262*\035\014\344\237\2461c\014E'\275\022d\226\\\2443#\255;\266\215\351\023\363\270\217mc\343\223/:\337\266\3163)\261\224\032\276x`\362\0020\326\023\355e\237c\207\261\272\354\377\354\227\210\231\031\302\340\371\230\2111\233\225\250\243\255j\313!yQ\205\362\356;'>\357\211\020v\215{<\016\202wP^{O\177\206\3260\030h\322B\327\276\312\344\232k3\322\275\204\317\275\366\201hdz\020h\3005\377\001\301S\2634\217\305w\335\3106\353\240\202\242\001\020\227\274\322\366\241\343h\273\275\211\013\337\254:]BB\023\356:\206\312s\347(\231A\220\260\0076\324\235e\203\314w\300\223pGz\234M\2342\023\226Y\223M\241\256\367\332\250O(v\302r'\013\016\301\017\330\013Y\212\004S\337\355\351\017\040*\315\327\271\332\021\314J\030BB\261\376\331\247\001\215PM=G\0237a\354Z9h\307\004B\010d\302\312p\261\366\227%\264z\010\024\020\264i!E\010\333(b\267\356\033\231N}\204@\355\307G~\014'<\240/\373\364\333\300\224)\207|\260\351|<_\310\210\221\236\372\204~rX]\312\032\356\316\350*\351\347\014\206\016M\302\000(\333~qeQ\037\035Lb\330,\210\015\365\215\243\001\225\207\007\226\3029\273\362!R^U\032\"\242\255\302\203}\325O\026\0138\267\262\246\022\005\370\333@A?C^U\353x\360\0322\323\022\250ur9b\242<\251B%\0330\300#\271<\2046\203\201\370\313M\332\033mF\000\254gWn\220u\025\272\003\310O\023C#6\301y\330\230/U\315\245\302\226f\036\222\314\265\3748\210\004`\360\320\245D\340\254\177\333\024\034\354\233I=\314\307\3452\025\006\342YR\336\023Z\013\311\035u\203:\263~\203\301\373\350\274\373?\035\264\301I\353=\177\261\365a\253\333\013\320\266\203\321w\270/\012\\\311\325\204\0266\372\231\210\302\006\305\024\205/X\274\243w\377\207\032\200\310\372\320\347I<\024\336\373\033!\2745\251\300\263\326\252\310\341E\222\225\002i\271\343\230[M\267\3139\346\253\006\304\023:i\031\362\373\276\304(Z~\223\3729\256\252*e\373UR\363\040u\\{\010\267\2733\224\203\017\345J\322\241\275IyD\222\002\246\023O\017<\363H\377\223\006\361\357\274\023L\345\001D\027\365\276\332uN\3666\267\3075\313v\005\366A{\267L)\373e\252\325\360j\2725\225\212\300p\325A|\345.\353\273`\025I6y\2410&\013\262d\366\272\004\365\273\316\322*\3024B\3741\243P\034X\347\001\223\231\272\202D\036\337\352\203+\332o\277\371R3\242\033\332\203\253l\2226x\241\231\330\236\360$\326\256\211\023\273\227@Q\021\253Z\203\305\312\262\325*\240T2\016<\270[\003>\303O\240\220\375*\340\363\344f-\310/\223\2336`\363\"\207l\273\240)\266\006}Z\344\257\3717\253\300_\244\371\370j\236\224\037;\355\002\336W\360\3360\266Py*\340G\277\0058\241\225\271;4\314\324g\362\306Cy\306\204\002\023\344\237\330\206\271\306\022\340\3758+\210\3322\212\271\030`\240}r)\357\277\243]x\362\000;\314\252d\226/\347\215n\014\354\333w\273\037\272~\257I\334\327S\250\3634NO\213\272\305\220z\253*\004\0062\215\2528\007\223rx*mP\201:\254\021\257\341\362\016w\014\040\025l'\037\304\006\235\314\300\227\243i/\023;\220\000U\311t4\274D\014\030\327f\213\253\204\3212-\371\015o2\256\241\224\005\244\036(9\276+{\030\331\333\231\3437\"\023?\303\326/~\304\260\201\261\277O\362O\305\307\264\334\216F2\265l\243r\350+\277\210\216\253\341\302S\342\177\320\3420\237`\202\014\221\224E=j\376v%\003XuN\371z\341\313Q^\341\351Y\\1\337\013\306\351B*\335\006\227V2\"G\224\235\331G\007H^\210'u\253\350Z\226\303\354;\367\207\230Z\212L\013\354P\242\221\000\023@\373v\260\007\004)t\313C7\315\357\266\002\327;\002\015\3676\2611\266\017F\005Y\364\307w\360KUX}\330\012\006\337\255\334\363\331\237p\233\344\025\\\373\301\030\311\230>\334\014\312f\236V)\\\236B\261\215\237d\306\266CXz\245h\270\213\200\370CY7\362*\367\226m\347eZ^2q\013z\012\245\021\237e\037\323\350\257\357\300\272X\026\237\030\303}\370\253VN\226\035\232@\003\256B\340\330\207\037\336i\237Bq!\004\307\2400\221\307?\257\311\350C%\203\330Q\253\010\201\273N)\251\012\254\217\005\3176\012\376Z\301z\006\232\212&4\264\256\263\011\3555\2258\200\2262\317\370^\267mI\002OB\356&\376\202^\034/W\337Z\263\226|\030\201\340\362\303sz\303\3523\357Q3\305\335\216\035\360p2\301\244\305\235+l\322!\206\341\241#0;s\306\"XC*\303\004LP\237\352f\303\241\020?D\322\017\356y\365%\352\364;=\2510\337\006R\361\351k#\242\252\327\201\310\332a\254\313C\314\206\336\3237\030%k\253\236\177\353\263\324\252X\333\330T\015\2568Cs\255\267J:%Z7pV9\375\261\036q\363\036\341q!c\212kL.j(\341{pZ\243?5\304\211\256\310\302Z\314A\350\273\000\014\015\315\376\014\216wHG\311\262aK\031\260\333\300\305\337u\203#\230\040\033\205\037\235g\275\030x,\224H?XS\361\016\301\371\315\335\025$f\335Fd\024Q\264\025\257\276\365)\254\306\367\274f\203\342$-\017\277\245o\361c\234\366+\016V\257\356\255\264\310}\205\342\025\372\\K\272\334\022\200\217\347\005\264z\245\022\327^\221k\250\342-\307\034k\343\327\033h\243\212\2151\256\254\005\336\323\362=\010V\213#m+\327\326\265\020\023\3335z<\215h\345\270^k&3\250zW\241\374\206\261\3267\323a,\255\305q\3455\301\223\205\2415p\2620\264\003]|\237[\214\017\0010\214\242\245\352wQf(>\366v\377\363?\277\337\333\373\323\343??\336\363\224\352\223\355\257\250:\030\037\213\335L\333!\244\351\252\246\003Z\340T\216\360\376?\347\004k\274\336>\0404\327H\177\004\3375\244t!\337\030\2766\2269\0408\261\253_y\353\275EpJ\243D\263\221\267\234\\/\360\235\364m\250\260>\234\252$k\234\003\331V\001iPEj\365e\376\252\234\244\276\324T\260D\241\274\036\2422\030\234\344\317R\314g\015\032,~3\360\361\212\270g\315r\246\377\312\366Q\001\037\270\274\316\332~\264\330\325\351\366E\3729\033\027\227e\262\270\002\213\000u\355Kj\026$&\20608d\331\017\324\267\20332\377L\013\361\242\337\334\253%\030\254N\315\032\327\2523\2767Le+\346\341a\211,\320*?\263\317~\311%\033\233\331\022\365F\237\311\216\3521\002\337\022\031\304\361\021.\341\237\210\345\327\213\334\372\214M\244V\340\206\024i\237N|\245\026\311&\245\275\226j\240;3\232\377\306Z%\\\035@\301\014v\006\"\274\226/\251\202W\262\262\035\233\365\315\365J\251*\3761;\220\\d\365\213\246\253\206\203\352\273\017\2616Y\302S"_buf, - "\004\374=\335\307\345U\325d\225\344\025\006\257*\356+\304\264\225R\374h\376\316\364\000\345\256\030T=\024B\301\254G\373\015\220n7Z\365@\010>Q\370yh\337\015U\247\014\254\177\017\013\323\202\226LhN\315:\014Hs\021\015L\020!\242\012A\242\363\3346\201\350;.q\015\274\326(p\202~N\001!\250\353\250Y\205\247\361\263d\232\272\232\251:\326xw&od\260\252**\214\002\020\223H\333\017^_\236\363\347..X\213\025\213\0327`\"\335\325\2647\360{\015\034\305'\242<\272q\354\302PC\215&\252gS\"\353'\037y\255\362l\011\272eR\247\252\264q\363\356-\223(x\254\267Z\355\334!\372%=\027\215\033<\017\005@\306\240~\330\322\335\023\322\304\303\226\240\003\226\306v\011\306.^e^p\003\214Fw-\341\254u~|v>::\204\314Mxf;\202HCo\015\001s\354r\014\312\026\307\272\351\363\014\250\376\013\235\371H\212d\343\336W\222m\177\265\304\320L<\206\233\247\230\321\246\012FSs\377\270\375\212nV\361\246\215\003/\344\203_\251\335\306\237\362\257q7\325Fa\350k\320\001\304\300RUF@\017\217X`\212w\317\276X\003(\210\214\2339\314l\011\233\246:\263\213C\272\205\230\011\301A\313\203UC\304\271\203\3027\313\021%\316\227\334\262}X\211.\311\366\355<\216QJ\304\333\300\303\352i\027\311(\203\373\040\002\302l@L\257~GV\225\215\000\012A\273\262\003\277\033\274~I?\327\340\357v\244\256\363\234h:4\351\276*\377{\231\314\320\020\3662\255\257\212\311\251\235\014\331\260\275Kp\341\217\255\030\032\335=Xs4\330\332\3342*&\205\343E\040\347\011\324\010E\216\224\020\372%\005\214t\006\003'[\262\021m\222\346P\005\210\355\221i3\220\236\326\021T=\362\204\235\270\240\274A(\3166\353\303\3012\025j\026w\015=\031\021b\0030\220u\3017-2\327\220\277\256\011\3345p\007m\341\300\203\037\323\262\225\025|L\005\222\014\303x=\241\265\345\253\021\341\263\237\353eIGp\177\235_\216F\335\215?\320\245sr9O\"|\030M\262\3442/*\260\355\302\026\325\334\"c\177@A\363\316\316\317Poo\207\321:\335\231\244d\201/\312\252#+I\272\366{\365DH\367s\240\011\245\320\333\320\3746_\024\343\204\226\277\365\240\301\253!x\335\317?]}\335?\223\235\330\037\2518\014\232C\014\327\240?=\3256\304+\266\227\322\037\274R0\340\315\030\343\215U\312r>*\213\242\346M}E\367\316R\024p\274\3050\216\034\242t\276\000\210[\266n\351\260vrz~\374\346\364\360\305\350\305\311\351\361\011;\271\301m#w\037\360T2\225\233\225B\302)\334\304\260\\\346g\220\376\222\275=\306\252jl\012|\205\377\264\321xtK\213\"\214W\032\351\341\226\337]\033\331\323\242\306pP+{\213\027\035\234\240\243\2331\344\2124K7\332}\232X^\3328\206\211u\033\356}\\\300]\024\223\\\036\0044\200G\274\331\004\333y\356\040\003`'\002nP[T\235\204zi\346o\223J\2760\244m\233\224+\211R\311\272\333\002\254D`;\34225Z1Y\265\207\035\371\333\247t\311\257~8\313\313r\040\027\217{\026\246L\207\250?\3674\334\026\011\040\021u\325%\212\224\031\026(#q\013\203\022\003(3\235\013\001\213%PC,\351\263N\343\311*1}kO\367\331r\014a\327\340\272qCZ\263\336\3549V\353s]j\314\316u\030\022\360Wa\262\252\307W\213&O'\255\203\007\272@@\243\203N\253\346n\256\222\352\350*\233M\312\246\276\240\254*o$\316\027A\271C`\223\311\004\301*\306~\255j\215!,\233\277\005|\355D\311\333Ya\206\012\234\321#hz\274\313v\314\036u\275\221\343\270Ua2>^OS\040&\357Ctb\270\227!\3576\255~>xi\040Wy\250\372\206\337\253\231\276\242[\013}\273\354\366\345\366\316\330\315z\253\336y|AV\200\025\016%6H\367\332\344\326u#V\021\366&\305,\035\372\207h\333\252\3216\260\343\347\2752\232&\236\213\021c6+2\377\012\031\3033a\223{[\224\314\256\223\233\012\374\372Er\036\236\247\001\015\200\260\005z$<\001Z\301\365P\366\205x\320\255\376\253\257V\271}\212\245\3479\255\373v[\321|\337\223\240\335\220\242&(z\2653\324\320k\312\221k\013>CUSBB\332\007\321W\277A\216\375\230\346\344\302\261\032D\003]\303\272\203Gs\003\237\264\363\0022\350\331%\305\003\204j\040R\020\253\361\254\250H\245\321\335\031\217\340i\224\344\020\276\2211\326\302R@\202\375\2438\355_\366\243KA\221\252kg9\334\304\335\262\357h\035\214k\310\313\3156\311x\032\3679f\232]I8Ph\204\363zN\234\246\351\244:\314\321K\213)\261\203\366\316\017r\026\006\036\016\364iz\356V\350\351'\334\215X\013\003o\364\024J\361d6\203D\252\255d8\\[\373\304tW2)x\374\351\333jt\033\252\036\367\025\343\365\314\210\334\331\007\356K/pO;Rq\006\201\202\036\362h%*{\234\314f\305%\270\255Pq\000\036\370\257\363\216\205)?,\266\356\340m\3761/\256\363\006\360\036\2633\256\351\327\\\332\357oXk\300\364w\206uyC\326\027\327\030,\271\311\336Am>\2270,\311m\030g\354<\300Z\247M\260\331\270O\250\234\223\024\233\253\002\014\245\030\265\321\261\004\235\347(*ZH'\032\365\210;\321<\214\234\007\217\335\353c\255'\345F\010\225\220\260L\025;wCnW:\243\347\351'6\371\027\344\223\3014\313t\262\016,t1E_\204\035^\034^\260\007}\026\200%\274\306\034\341\354\210o\343}\024H)\033\2329c\267\273\353\264Y\364v\263\223\257?\254\207{\201\201y<9|\250\332C\275Lk\345\307\024\366\346\240&\301\213\356\000p\276[\342]u\003t}S\015\345\201\265/\35666\310\345\334~>\030\234H\003\177\250\205\271\247\205\333\231\303\331\017\344\246\275\333}g\261\020\367\225\201\013^\375\326\326w\265k&\000\015\304,\311\352\231\232\223C+7\012\250\244iD\035\272\241b\3515\305\360\303y\245,\256\273Q\300i\303\366\2760\230H\313\227#\357\\)\350\330\361\224\200!\216\"OBb\210\340\034\231>\040t\321J_t)\225B,\333\211\333\213CF\2347)\233g\376\207\035hI\036\040\345\252\274\212\275\240S\007\271c\364V$\005\3221\277sn]\257\211\243o\270\221\005]\344\215\224I\215\356\372\202\"\033\315)5m\247\027\203\034Q\267\371\363\326\351\222T\026\307\377\375\031\203\015v\375]\360\253\341\001p\353\021$+\223#V\340\312\332*:\322\2512\040J\314\271>!\301\314\303\334\035v0x\235\324l\344\271\374\303\353]A^\022\374\364>\037\251%m]Bz\300\376.\340\272>\022\236\216<\000\362\025;\22674\226`\000\313K8\332\217\340\030\275\371\\i\303%\301\"\211\040!y\336\302\341\364\232)\331\343\244\234\310v2}\011\3678Au\353,\315+\246\225~J!\203\223\367B\327?\026\031\300\340\013\003\326\334q\203D\263\320s#\"\372\236\302\301\012\031&\021$.\352\357p>\275\373\242j\215Y\253\214D0\315T\323\021\375J\242)\363\004O\221#(%S\2744IS\314O\260\326\0278Z\264k\004\276\015\314\015\223^\040\003&r\334\326\203XPY\263\332/\363IZ\316n\030\261\371;wF\234&t\371)Od=\254\202k\265\211\203\220\003\363\345\040\377\225\223\366\300\203\024\330\236\235\370\037\177\024?!EJ\371\3750\020\277bA\033/bc\330\270\3443q\307\262\371\3017]\213H\335\262,\374\303Q\267\313\201T$\346\320\002B\326v\342o\010@\242\204\347b\000\326\331\012\205\311\303\003@\226z\332\367e;h\232\003#\306#<\331\326\331\274\011\342\275\314f~\303gs\265\301\022\347\222\306\040e\2406\217\323~\253iT\370\277\244\346\356\210\236\336P/\355#\213\032\275\371-[\272\350Vb\353\3306\354\"\216Z5\315V\206]\002\254Z\370\010G\277\272\036'ik\270\"\272Y\030\3028\372\246\245\305\315N\006\366\020-\206\311W\216\016\263\340Y\001\203*X\020\274\212\010Ks^\243\320\035\250\211\237i\246\241\010\345\250\353\215O!\312\340w_D\227\264\310z\026P\275\036\244\307\371DP\336\3407\342~\305cD^^\324L\327\327\230n\315Nk\311,\233\034\226\227r\221\272)Zb(\272-\333uC\305\272L\2258\244/\2170-[sY\026\201\"\346_-)\236A\375\216\303y\325kY\253\0053\371\340~,\253Ml\032om\375\227`o:}#\272\376r\002\345\245\345\3405/&\344\271\252gh\236\217R\330!+\262\031[Y\037\347#\200r``\2723\264\312d`G\006\300j\234,\250b\252\233kr>\"\227{\206\251\264fb7n\375\040)\374)\220`Uc\250\275)v6^\230\320J\027\310\000\212\212\205\364\347\223H\207\266\217\341[v\200\301\243G\3314\372\2241\255\032F\024\301\305\265\221\205\024-\270\362}L\020\337!\234\017\2606\035\3572\0121\241\260a\235s5\277;\006\301\215\346v\3751\264\213\200|\362\222\315\256\317Z\256\347\342\3219\312a%\201\225y\335<\231p\331\357\253\224V[e/\234MQ\300\326\350\303\223\257\330\326\324\230\363'\370f!\377P\244\013\326\344\342\205h\337o\271\246q\342\265\330\275\320\204\316\340\002\235\264\306\261_\322\231\331qoEJ\334\325\270\364\020\225@\371\364t\001\331K\254\273?\217\037\020\366aT\362%o\223<\035\270\2211d\204\200\227\234\214^\271\357\035\227\207y\250'H\020n\264\022\275\260\027\262\227\325p4\032\015\254\370;\207+\357>;\216\203\013\366\315D\255\365\344\277\227E\355CF\243\342+\360\301\270\0172j\323\252\3115+\353\255\274q\312\353\262\230i\335:\267<\232\270\363d\316\265%\334\303\207\015w\313\012\365[\036\332\027\235_\027\242.\013\247\003\340:G\273]\245\177\007v/\310\206\237\314\2601d\323\005\323X\241\273\305B\216\344\214}\271\254a:\261\0408kz\231}BW;\236\010\036\276A\010\2444D||\021\221\251\012\213\013\227\333]\241!\023\022\271Kh+\332\032\254f(j\372\273\325\324\334\017]\0160\374%-X\357,X\250a\234\246\327\270\010\242s7\313l\023\260N#0\305\345\355`z\335\303L\220\355\2019Wl\241\371Sr\304/\364U\336wO\316x}E\201{GJ'\371A\307\353a\333W\263\304v\367\033v\010\3633\315\320\330m\273\247\225p-^?eZ\332y\361\"\251j\037\007Q\017\373M\373\272w=\252\322.\372\262\304\354\275k,>\241\022\351DO>\245>d\345tj\303\015i\362J;\2657\242\356J\365\317fu\261\"\034\353P\2309\032}\\\375+\245A\245\322\275\260P\006\375\276\345\373\316\231}\347\273w\276\357\202\314\323\250[4\362\272\323q\307\327\261\303\353\036|1\213\022\257l\021\270\214\362\262\225v\022\260%\231\214\215{\316\355\023\232\3311\350\322\242\035\202\204\275F\013<\262@\006\023!\360\327\007\216\0052x\323\346\227}\306\002\221\270\316\370#<\002\353\"/\014\311/\037$D\271\342\004\350\260AQ\342&\317\023\022\212v`:8P,\203\371(\344\363s0\366\353\307Ov\222\245\323&\034\331\227\363\024K`\220\236\007\247\332e\211\232\037\244=\312\040\265I5.\026i\177#Za@\220\247j\343(\255\277V\007~\267\362\204y\306\367\275\367\035\353\033\027\276\320\205\235\355\320s\2265\274'\037=\252\310\207\177\225\225[\355\365)\337\356\325\225\244\337\324]\027\037\261\034\2131\\+\355\221\236S!#+E\206\026\012]\012+KEf*\013\242\007\374W\2445\330\355\231\237\277\313>D;Q\006\316aF;_#\326f\257\275Y\307\256\201\002\320{\232\316\326u\005@\263\306\346\035\317\237\273\201\263W#\023\2550\015\011\036\200\216\366\333\210AC"_buf, - "\333\260#\030\005\336\036\336\260\034\234\037\320\320\270\204\264\351\343\334[\303m\2158@y\234;\275\367\272C1\021\332\"t\322n\200\304\220s\341sMo\321\247u\3136\214\371W\315b[\337(\224\354w?\275\335h%LV\2302\035\351\265b\232u\355\360[\3152;F\237\320\221\034\256\236\005\251\263\012\037u\2608\022$\346g/;\340\227Y\326W\360\223B\330\336\365\247E\361\241k\303\273N#,\205\003w\017\214\357\353\024}/i\013\221\345\226\020*\002d\303\200\276`\205D\2142\016o\360\365\247|\034\241\250\021<\343^\367X\350\310\215\240\345\243\306b0\3647\277}\011\244\241\275\033\253\253)\032\306X\307'\314\347>^\017\244\332\274O\216\367\347/\275\343:\360\244\364\274\037\332y\245\304\306\232\244\273O\262\335\376\353\213\015iG\307+\030\361\253\325U\214V\341\251\361\336H\224\335\355\366\3715\017\002\352+C}\310\2376\224\\\310\276\341\262o\301\262\271\233Ih|U\026y1\334\330\320\023\265\253\244\353y\222\027\224\267\263:\311\317\360\017L\331.\376\267\337\354\276z)\203\312N\025\2403\326yz\274(\330Q\260\013\316\252\2623\377\2359a8\030L\226\2244\\\253\311%^iX\016c\363\253\253\354\362\212)\010U1[\322\307\263b\374\021\034_\257cFjF\221Q\005\350\214R\216O\267?\206\314~\226+\200\257~\000\016\220\221\235\362F\001\3307\262\233\340\3000\337\266x\\-\347\226\302\033\231\011\363U\323\254Ni\364\225\"\277\265\206\021\037\234\357sJ\254\277\222\370\026\000\274D\013\250\251\252\177TN##(\333\031\030\033\300\307\252!\225\377\005\270$A\233vX\332p&E@\264\327w\207y+\"\3738\214\003\015I_\276g\244\366$\235\325\011h\261\330lG}\342I,\315\346\372\341\001}\341\001G\232\202\330\334/\040Q\355\230\235\345.\200\350L\322\300\011\242\200\200\215\357\">\232hg\207\214\357\300x>p\240e\244e\231]\314H\311`\260y\350\007\204\2012e\240\357\373\352\374\351\263A\364Sqm\\\022$5v5O\223j\011i\274\324r\202^\230\206t\221\\\314n\242k\266\342.\177\360n\313D\240\241\306\236\017\331P\266}\342%\264m\013\221\300\350\370(\212\351\000\263\214V\356\307\326\206\312\206\370s\272\305\216\302\277.+(\254\372Q\334m\260\301\261-\362*-\323~t^\000\213]\244\260\343\000\335\306H\266\232W#\204%\322g\023\371\011\311\312\217@P\247u\226\245ee\367\266\203\255\026\354\014\237C4\021#\023\003=/\030\006\332\222\316PI,\323\255\212is\040\261\242OI\231A\205\217\376\206\237\006\217\264\005\351\265\035\012\361{\314\005\324\244\255\204\342\362\207\244\211&9\031\233\007\205\235c\221\326?\364h\343\260\021q\213\264\256|\203o\256\344\207\366\313X\0330\207\014\343\236%\213\012\354&\362C\351\007\326\270\345\264\330\261L\376\376{=\201\013\225\203:fCI\362\234\035\371mW\224\234\355<\207%`\034=\205D\276/\262\272ft\277\265vs.?\261%S)Pdx\012P3t\340\346\307\311cj\232\015\240\212;C\376\342\206\035\356\257\361\030?\241\022\357\371\316\337\323\262\350\341\356\224\344\020\267\274\234\247>@\270{\317\010\325\024\307\006\312\004\233\036\200\263d\013\255\357Wm>%\263%\240\267]\246\030\304\274`+\224\204\003\\\357l\017\343M\206~\270\342\015}\376\203\040\322\000H\266\252*\222k\221-\223\353\227\030\230r^\360\312\207|6q\033\331\246@\237\236\241\263b\275A\235\342\020\371\012F{\250f\207)\225A\2272\011\222\260\271b\373\351\230a\236\252\015\034\246\0105`,ek%_@\301\310;\203@\243\2619\223`\301W\2344\030\010F`j\245\376\230S\307\271\036\217\320\207\177\307b\015\302\202\372\322\337i*\216\024\334t\007\207\264\332\006\356\261K\333z\033\016c\242\2506\255\253\023\247\213\244\351\273\2371\311\214(I1e\233P\274\265\273\325\225\017\331\352\263\334\301\341hA\341\012\360\327C\032\233e\242\344\340\005\330\353\3701\207\350\216e\030\343H\337e\037\324\000DNv\221r]\260\032\243Y\215.\263u\372\244\276Y\244\350p~>\334\320\271o\272PlwN\034\335C\256`ka\234Uz\226\004\320qym\270\254b\312A\214\255\273\036\253@\207\275\355\030\014\337Lbk\374\262\353X\307bC\253H\301\303N>k\001\335\354)\342\263\357,0`(\2136\366\001p\202Y\026Fp\2415\312\213\032\335\327\331\274\012\315\033\335\232a\026\335<\316P!\005\336L\270}\021\222B[w\213\023\270\215\0403\2435\361\231\356\3774AP\312-\217\237\246*\017\344\315\317&h\310\266\306\353\223\034\3349\031CV\261\217\201\267:[\354\350\305\040\263\377\312kj\217\354\256\342\316{H\027\260!o\300d]>\015-\345\026\300\336\273\236^\357s\333MJ\324n\355\274\177\237wZx/n\275\257\033@\324+@\370\375\2524\263\344\270\273\302\203P+\240\240\372\225t\021\2727\262-S\325X\177\246\322u\364\372\365\336\177pek\364\227\223\343\237\3330\305\350S\226^k\234a\277\322'\200c0\030p\361E\032\230h\236Mo\364\257\277\340\274C\231\255[\251\364\205\360Q\033\313\266\316\244\352\251\315\235\026W\334\0211\262\345X\231S\010\326\027\246*b2J\372\346\266\2434\217\333\306a\330\003\370\027C\335\3138?\037\375t\370\246\231Y\256\233D\310\265\220!\327\332\340\214\365\315\267\013\351\271\017\015M\307}\255\334\017[\365\327\366\262\207\035?\036GO\230\212\365y:\3552mQ\337\325\201\320C\220\011\203h\353\207-\217\254o\"o\227\026T\364\365+\352\272aI]7\257\251\026\004\327\037#\027\321\322\212V\254\255k\040\316\250v\227\227\361\"\272'>\275\376\326kL`\355\033\311\277\332\030\324YWY\015<\374\365\364\177\316\217\273\372\201\230a\301\232\017\233\231\015\364W\233;\360\354G\352\344z\"\273.Fpr\273d\300\245\266?+X\267\360\237\241\320P\225"_buf, - "(\207\223\322\312\021\005\361\207\003\221\302\034\264\344\265qVk\337\217f\250kl\250\372\306\217\377\241\235G>\014\\4Z)\372\232\242\016\334N\347\350a$P\326M\030\3062\340\247\260(\326\317a\254\265\204\011\177lu}\202T;\000\334\206'\330\260\011\252\261\032F\305\257\240\372\012\026]\211\2255\005&\270\1772^\315\310\375oc\024\360r\324\006\213N\217\027\026\361/\330f\337\001\257\260\016\333\336;x)\337i\004\252Y+t\001\251\3310\014\012*\212`hY\271\345;\211o\275g/:\306V`}7\015}7m\376.\017}\2277\177W\207\276\253=\337m\275g\247p\246<\321\367\354\260\315g\017\342c\326\335!C\246\177\311\351\336\355\222\224\371+\006\350\335\0070\304\262\216;\332\361\013_\354\301\033\215Wu\227eh\320N\003wN\020w[\314:\0271\325\262\335\012\266\372\267,gwC\304\300\240Y'\340y\372G\265\255\030\310\027\026\012\035\376\202\026\024\210e\035\346tV$\000JY\217\016\"0\203\207p\020\355E\327\370\333+P5s\031\267\225\311>\300#uk\272\345E\210\256\202,\214\366v\033P\222_\010\234\370m\322\272H9\264\307\013\325\021\375\040\342'u]\260\216\252\233\371E\201\011\255%\221\223\016\344cm\376z\232\316C\237O[|\276\310\306\201\257\027-\276\206+J\377\327y\213\257\347p\375\346\377|\331\352\363\331,\363\177>\307\317\3035\323\2766%\237u\007\250_\3659\327\2005\333x\032s\001`\225\317\212\377\203e\022\260\252@\264\023\305\342\035w\002\303\304\337\236\230\027\336\212a6\325\314J\354W\177\221`\326\261\203\210\377\261C0\370s\253\361\024S\246\243\003\033\376!\032O\255T\352Z\343\247\313\372\325G\371\005\3752?\303g\216XV\250\336\352D\330t\251\000\347\367\000\031\024\001\370\350X[\377\360\370\200\344{{D:\372V#?\376FH\274c\322\026\203\300\311\017\247\206\021XK\364\"\247K-\040J\000Mf\263\327\370a\030\260\230\310\203h\027S~\350S%}\301\374\240_}l\005\326tcD\036\347\377X\014\314\337\255``\336\312\303\300\224,\231\373\244i?\004\223\251G\326\207*M\312\201\2262E|V\233E\350\2338\223p\333t\207\207\234\351\037\237\235k\037Q\226\214\345\303Y\241(\233\205qt\271\317\232\002\364\000\263\221\203\250\034\376\244q\006\340B\026\263\362\357h\237\230\327\216\326\324\010\3111d\374f\336\260<|hN\206\263\362D\276\204\000Hb\332\326p\255\305\352\204\354:\237\331\202\302\307\004v\022\030%\321-Y\277\314\307\311\362\362\252\036\311\364\217\\\360[\355(\035\217\267\371\025on\226\337(\0267e\306Z\322\276\361\0302=^\025e\005\015\237\201\227ov\261\204\232a\230T\014\257\364\237\026lK\212\316\212i}\235\224i\364\"\033\2479\024\034\377KZ\242\362\263\327\337\355S7\361Y\232F\311\030Rj'\371\015\336H2BF/N\216\216O\317\216G{\243\335~\375\271\216\0120\250/n\242\244\326\260\273\252\353E5x\364\350\372\372\272\177\001=\366\213\362\362\221\365i\027\307s\366\372\331/;\034\215\235\223\011\370\251M\263\264\034DO\317^\3540lpG\316\035;\352\333\323\243\303\267?\376t>:\376\345\350\370\365\371\311\253\323\263\321O\257_\373}GB\215\315\315\376\345\331\321\350/\350\177\202'8\361;\0322\205\360?wwAc\370KV-\223\031S\007\227\223\254\210\036\357\356}\017\343\317\323\353\264$\015\301\350]\226E1,Y\036d\340[R\037\\_\026\311\002ny\324\305b4\313.|\334\322\215\336#\217B\2558\323\250\326\032\247.\030\260\357<\036i\322kD\323g\307l\217`p\214\374\313\323W_\361\361:tiI\216\015\215,\255\330\323\020.+D\203\237a\\\275R\356\354\036@\350*\024\264,?;9C\017,\235\024\376H\311?\244\263f\363\264\227\254>\347{?\222C\3202\376`\210\360\346\217\300d\304\347\000\017\373\255\012&\207d\266%\262?\221\340\264\003\033\012Y\3238\240\333s\201;\030\360?\344Pb\335\221\006\216\253\243y\362kQ\362ff\006N\253]\226\267j\267\000\010\257Jl\262\373\313/\321/\276&b\356\361\254\305\230\326_Y\006.\\\351\014\364\217cd\312\246\224\217\237\362`e\363@\356\203\017\333y\231R\342\024\366e\261\274\274\342\004\237\245S\\\000\011\252\013\311X$\303/(\232\363\202\235\261&@h\210,\377!\270\317\210\236=}1zy\370\313howt\374\313k\004H\377\377;\370w\337\314\266r\261\234N\323\362\235\001\343\203v\376\000\357\012\250\040!\313\374\245e\231\027=Q\371\017\317]\230B{\032M\313b\216E%`\031\341\215\253\"\3001|\365\343\022n\341/\341\277\373\"\307\240\210\311T\0075\2026\252bB\255\027u\376\275\377\335\264\323\223\323\327\265C\305\210\030\364]\363W<`\314\2150\343\031\363\350\343\240\377\335\331Uq\255\270\352\204\322\234\212-\226\302\371z\215\314\206\326;j\330\2574`\025\245r\326\341W\203\301!9\015\005\034F\255b\364w\354\343\024\303\206\002]x\005\247bB>R\306\241\214\353x\217\354\207\244\220\253\317@\323!\317T\040\011\004\027\241Y\036\316\373\232\226\230\0277\245jN\"\250\341S\012\231\353\215\324\265\332L\200?\2755.o\022\254B\317<\2153=\305\213w\333\024\"3OB\212\005\252!:P\375\250\263\330\027G+\177\200\000\335{\233\242\022\207\233\026\245\247\005R|B6<\200\010\253\306\340*+e\265,Ku\014\313\370EV\325)\033\313S\364\347\365>\216\337\360]Pg\374h3\342\221\254j\214\003\242.#\263\370\002?\027\355\314x\274*\255\237\374%-/\040\014\346f\030B\210\011\366\263\345\002\201MD\353,\255b\217[\344\227H\202\033\014\376{\231\245uO\177r\212\361\013\306\243\237\240B\312\255\277\312\237\007\027\231\271\000#U@n\310\215\367$\237\026\202.j\234\262Nk\023\260c\330\214\025$\006[dAAP\033\341\265\331\224\375\306Pq\006\333\011\364\024\241\361$aZG'r\335\274u\017oW7\325\031D\322\250\027\315A\277\277Ly\361\241\272\032l`\001\243k(\2160\210N\013\314\204R\301\005x\237\336\300P&\203\3509\245\030y\007h=b\030}\210N\261)\372>\202m\327y\377RK\276\3227z\241\3749!XQ\234\027\332\247]/\036\247\034O\263wO\227\227e\232\346\341.9\204\350:c\3246?\347\212=\323`(\005\213\035\214\261d2\214\030W\244\250\241\324$z~B\236\254D%<\021)\211\300m\304\210Z^\222A\302\230\0007\037\240\003O\345\004\012u\3255\022U\001\0230\254q\011\010\206\247\223\011S\007\213RS\017\334\302,\177[\262=\177z3RD\032\211\356\255\272\277\210\211\233\237Fa\251\275\023\024\371\3012\303\211u\026\202Fag\332\202\332\267\210i\300\353p\276A\213\211\003\334\246\247\023\301\262\2301\315\010\002\354B_0\345Vrp\207,\344\354\221\211\302TC!LK\013q\247g\207\020\254\037\371\214\272\336\352o\2058\307Cx?+\232\250\363\345\263\212z\202\345\326\040\037\377\304\244\237\375}\307\221\011\235\026\003\024|r7\376_\305D\367\316\022\3671\323\355\206zF5\322W\014\266\345\234\323\266\274\316\224\323\027\016y\314\336I\"\257$\214\204\025$\014\025\013\001Wa\272\030\224\265<\216H\235\022\352\260fP\331\030\317\030\270H\352}\257K,\217\304\250\273`\247\253lL;\267\375Z\271\030\035D\261\363-\235\203\272\021\226\207Lk\256&\332\315\356\374\235\2717\215\204\275\306\247\272\342k\366\213_\364\214p\213\003%\371%\251\006ZP\302\200\033\224b\016P\275\351q\253UL\260\324\224\020\207\351\015\271\306Q\211\246\231\267\253^\224\325\374\251\257\241\254=\255}\340\040\036{\306b\206\025\341{\343\200\202O\316\330\342\030\247\360\235Y\374[`\004\267@\026\"zU1\036|\243\354xD\204\363\233\205\345D\214q\"\364\362\347\204\235\364_}4\225@DF\373\330\273l{\226\212l\231\270\021\306\2532cX&3\327\220hu%+\316\371\214\216\306\231\231\217\356*\251\364\206\236b\363\004x\316N\341\220\315UL\215\030\312\251[R\314{\274\016\200iQ\015F\243\257\302\224$\367\300\031\026\037UVA\342\305\340`VM\210u\316yh(&\035(\032\313\344\353uRE\305\307Nw\215\321;\375\342&e\367\367\355\031\340^&\003R\225_\037\2134O\355\030\277\365xO\252j\231\306\235e\236~^\244\350\236.\023J\321N\302\327\357\240\343\373Z\010\036\337;E\0256\212oB\230\347\011\333\325p\244G\005;\327~;\352L\241#\260\026S\\\027u\365/O\236g\331\204\035\301\256\312\342[3\217\313:=\364\250fG\355\264\363\317\031;lI\355\006\013r\225\251A\260g\256=\207_\213\345\317I\011_\257\205\3505}\363\017\307\225\315\031\323\340\262\0326\204e\231~3N\302Nf7\235vch\330\035=\376\356\024\371S\245\334v\001)F1@J\273\000\002[!C\266\2148\235\253\040Ax\372\231A\203xBJ=\315\352A\003U\357\2622;\333\333\021\252\257\271\024K\333\333\235v\325\3266\030\354O\211\010\262S\312\235\256\312y\022uk\207\"\274\372\212m\253\237\341\237\210\367\021\3544\2414;\003<\2343\006[\256\215:4\354#\212.\040C\2347\271XU\275*\201\336.\352\250\203\251\006!\267]\327\237\3103f\372\333\023v\242F\314\035\274T\207MnH\332g\203\255&\177\012m\215x\206\237\301\013O\032,\033)l\027\240\272%\222-\002\006tX\213jZ\207\373\326M\320\012\362\306\232}\271\221\274\035\270\334\346\010\200b8\3504\321\267\205F\027\240\265\357#/k\255M\031\232\012\303u\241=ZA\265s5n\216\177\304\232+`\215)\232\026\345\040\352\264c\3746>\034\015\004\221;\2365tu\002}\240\035A\241\300d\303\334t\242-\264\233\310\217w\206\302\352\217\356[[\026S?|([\266\230\271\320\326\245\01170VhT\016\216\351\300\031\223\347\306[;h\023s`\024q\206\031\370\264\357\307\010`\337\327\366\324\312\362\247]\334\014I\016M2HH0N\025n=\352\241\253\237\363Mz\267\225\253\372\274\370lZ\247l\037\344\243\350t=2\324\216\3330X\202\220\2640\023\245\305!&]\230e(@\235+\003\274\362|\224\3447\321\311\351\363W\222\212\316\361\330\261\247@\022\004\215\257\3003\021\360\260TV\237_k\223f\347ew\357\330\032\250\337\274\260\355\251`k\264\323&&\021\034"_buf, - "\012\262\334\016\\\276]c\001\031\212\212i\247\243\177-\223\036\321Rl\212$X\264\262\315\334\311\202\367\007\324\036\312\371kn\206\305t\252z\004\016u\030\011ia\215\346@g\306\3677n\367\003\227\216\246\263\017\355\345\226U\025\275\004\236\245\325\270\314\026\216'\243vO\332\241\017*\272\007\244AW\021\353.\211\300\271\2151>8\0162\255i\231\325\3401\202\336\037'\317\216\253\216w\255\240\300rP\311\213\227p9\312\340\235\013c\264?\257\021;\007F\015\362\225_\322q\365\233\022{N\270\320\305o\271\230\0158D\256\304\323uf\210<\336\014\233\2212n\312\351\013\303\"_\206\220E81\037\333\203odN&^\255\357ms\260\345\036\343g4\351\340c\013\263g\340\277\0107\024LD\360\036\301\226V\221\365m\272\234ar\202\353t\213\2114pCD\3100\235\365Ua\331\326\320\375\177\316\235^v\206\334\307\363LB\"t+\212\3600l\203\336\200n\000\347\267\372ZB\221\237\220#\177\254\270\337\265\313\206\333\202\177\320\2211\362s\206\"F\300\375\025\277\304\235\250\032\220J`b\307\177\300t\364E\230M\200\256\267\355\211\011b\353\315R\310\202s\372eR\263\326\037\332C\323\335!\024#\351\237\320\205\256\303:\332\311\341}\276\3250\030\257\233\232\215\270\205\244\227\233\234\261\377n=\211\225\370$\307\2517\307?\236\234\235\037\277a\177\274~\365\206\375\021G\035\356s\325\3519\327\205\335\265=\274t\377\255\260\217W^\025\263\324\361f\017\272\256\207\252\253\360*j\\\343\213!\334\324}\312\364^\330L\007\177\334\375\323^\027\006s\312\304'\270\033\315\222\213tF\226\245\343_^\27789:9\177\361?\321\025\223\2633,F\300\357\2746\3707)\3452b`\036\303M\275\003C|\010r\232\247\035\003\205t\236a\316\236.f\301\317\001\223\211\267\366\262Y!.X\037\216\015\021\350\312V\033\323\031\346d\201\273\351E\027i>\276\232'\345G\326\325\016\346{\272Jg\013LdV\312,\200\262\3730|R\232'Qg\347\347e\276\254\322\311\316t\231\343\"\355H\254We#&\364P\0300\365\213\341\210z\314\021M\272`-~\375\314\237\256\270\205\016\2642.\243C\220\374w\313\201\326\367\004\345\037tQ\3353\352\020\210\273g\355q\363\365uO;G\032\016\024h\240\325\222Z\360\363\214R\002\344\011\307m\345\277\001\357m\0047z\377}\3667\270w\226\307v1L\256\021Y\333\2774\006B!X\341\374iik\232Jt\225T\312\256\321u6W\337-?\017\270\006\177IWU\342d|\001r\0050\320/\256:\373\353\300\037\256\013\276\352|\323{g\217\002\321nJ\334i!?^\363\312\331R\027|q\346\336.\321L\337\256\303F3\331\377\362\331n\272\330nC\323\026\364\264\261\235,\361\266'|\335\275\036\205Z\314\000\244\273\370F\324\327@\257I\371\2257\347\337\226\374I\344\275Q\357\334\357\365\3667\032\303\005\217#\314\013\235w\022HKYB\254\3405^)\262\355\016\037\012>\353|\345\365\265\215\005\336W\177\375e\263\263\232\371\355\362=^\015\257\246p\233\211\272\027Q\250\256\227\243o,\025\003=\255X\246\377\377\272\2436&\326{#\275\3764\207.\255\035GD\373\276\313\343\216h\344C\320\216\330>_\356a\264\353=\262\353\212\3417\365\026\363*\027\346\341\337\027\017\347\273\034\010\337\361\033\206\266;^\224\257\177}\2431\012\330m\006\357\363\216;\303\337\364\372\265\301}\301\355\251\341\032\312\326\016\002\327\230\034\320I\3762\031\227E\334h\321YE\211oy\343\253\015\015\205T\012\337`\331\307\367\366^\334@\301\000\206\301Q\263\357\226\363\033\3605\357\357\016\244\377\007\373\016u\244w\2017a\251\367r\3200\217\340n\320\372\366\320sQ\277\357\3047\332\016?\372;\235\235\202o\327\272\251l\272q4\323)|L\337\200\345\334LKK\305U\215@L\334\010\005\317\352/\320\360\036\361\0004\254h\376\203UX\370\325\351\331\253\027\307\243\237O\236\235\377\024ms\350\321#\376\305\000\312\341j\267\0301\007\310#\347yk\334\177\177\210\366Xs|\277\017\321'\032\032\020\251\236O^&\237c\343af\014b3\372\325\372\375Q\217)\314X/\277B\237\360\307G\247\244oFh\312\270\250_\275\255~U\255\354W\0379\322\040\011\371\272\302KG\302\000\252\324E\377wYA\001\3611\305\322|\211^\244\323\272\027\275\301\032\351\267.k\250\004\210P\306\366:\233\324Z\262\177\330\023\303F.N1\222;b\016\310\000\323#\020\010\200a\003?\006\003`\023\335\314lu\035\033\277\214l\307\010'\306\377\272\231py'F/\236D\025Vo\321\223\246\251\364\234\346F\346`\034\232\3511(\255;\224\223\274\272C}\005\335\271\307\263\326\235\251\205\371\025\303\003f\\\335\331Y\250\237\026h\242`p\356,\371?\250\211a\216y\251\203\354\014\005\343\273\266\177\016\3351\366\273\364\037\370\356\342m\202<2\034\345\250\327a\334\304t\373\376~\325T\334k\277\212\367\274\375\236\335{\227g\215\243\304\231\274\327\001\"\367\251\336\2745\232\374]\370\225[d'd\221\303J\306\247I\266\3227\351uyK\353\332\213^'\327\255D\3530eg\031\374r\005[u\346\201/\233\030\243\023\354-<\275\235y\247\335$\2552(\311\305\217G\2612\003\267\001\2343\243\\\201[\255@\3529\\\321\227)\265\344\264\312\346_\334\254C\000T|\321\027\302Fyu\313W\026\343\310\210\352}\345\372`\373\004\222\262u\016\267\351\352\302\332=\334@\276@\367\250\240\264\315!\024#\226\277\364\246\205\236\221\016\341TJ\267d\037-\313\222\235\372\010\022\023\272;{\332Q\203-\031+\367\243\241'\350X\373JD\204P\325\0205*3\210\014\356\272Z\240\265\345]\314\213Oil\302\320<\373\370\311\\}d/_/>\310\023\316\304{H\252\333$\031\313\345\216\007\372\003A37\343c\346OQ\016\377\303t\240\300JBGGo\261\177\373\267\177#4+^\344\207\375bx\260\307g\300\220RZZ\2106\231\356]8\017\037j\374/\210\207&\004N=\015\303w\356\347Z\251\040\374x\221L\2600\337\001\354\203DX\310\235\371$\034\323\301\273\243\365lYG\315,\\FKF&\275\207n\017\366C\363\353`\312!,\272\307\201\031v\014\230'%\233\007\0030\225tC4\346L\206\227\0154h;\035\241s\304\320\276\326\276Q\200\214\257\357o\251\010\241bK#\017Wy\356\244\202l\325\2121o\303\343\001C\220\345M\007\006O\343\2017e\242\310\025*\266o\177\266D\336\206\366s\310%\255\351\013yzm*\020\"\337'9\246iJ\300\273Mz\365!\3566\355\342_,\346Bh\323\345lFx\307\335\235\241tm<-\016\231\202ySe\225~\033\340\213\264p\335~\276\200/\000\207\203\232\0348\273\206-\243;\321\037\277\3539,\035\335\366|\200\351<\221@\356\235\212\201\335\373\243\371%\267\030\006>\2458\025\270\337\\\373Sq\233\235\344\341OM\207%\037{\031\213\355\237OLNG56\025~\303\316\040\237\326$\222\242.\373=+\256\005D\370\363n\020\241&\357<\001g$\366\277+\310\337I\040\361\317\225\040\233\247\343\226\3615*\305\316\352\376\335^\336\344\211\212G2y\215\040b\223\254o\003\261I\2151II\005\031\242\263\034\003\222*H$\005y\371;h4\347N\367V/\276\260\243@\324\221\272\271\370\332`\243\0406\344j~B\365\233\017\313\313%$\305\362!\224\224\227]\007\033\376]D\211\177y\344\002k\331\266\367VyZy\232V\036\320\024\202\020N\316\352\3043\011\262\336O\030\023\342\305\035\361y\300\020\226\011i\021U\364\333o\321\003\303\365s_&1]\035\345\204\036\235\276(\247t\336\227\267C\017,\304T\034S\253\010\245n\040\247->\236%\177\277ym\304\376\204|\302\027\322\333;\020kdb\311\025)'\276\2501\254\310\010'\0122\234\012\252!v;Sq5v\000\221\036\271jn\356;\377\257\275+on\3438\366\177\207\237b\265.K\200D\302\222_^REZz\005\001\220\304\204\002\031\002\264\354(*\004$\226\344F\270\202\005$+\011\363\331\337\364\034\273s\364\034\273XRt*\256Tlbg\272{\256\236\231\236_w\277\340w\257\003\376\221]\371\330\327\211zC\2628\260\350r(LA~\247\370%\275\237\234\3223\\\230\354k\304\3750\332E|Ei\267S\006\275J\040EvI-\030^|\264B\320$E\363\227y_\216\370H}N\030\211\330q\032v\023(\342\015b\017\365\302m\336\357\207\025\223Ck\214\314\310\233\235j.gE\246\033\305\323Lu4S.p\036\3672(\276\245s\331\315N1v\312lWS\2218\026\302M\300r`\363Z\235\303t1\313\356?\312\371\251\327o\277$\307\247\227\275~\347\315\333\366\351\037\017\373\257\233\226\265\223\037\304Nhv\016Xu\226\234W\324\210\224O\364w\351\232\014\306\032\362\324@rrA\205\036\356\231\035\213\336K\241\"\231\371Qnm\01224\031KS6\355\275\177\372A5B\355\374\246\010\200\177\3042\2470}'\031\307hF\225}!\016\263\207\025Q\356\241V\023\254U0\235d\326\315\0026\303\315C\376\037\300\366E\327\034X\347$\231\304&a\341B%Dh\335\330\325v>v\271\302\314\307AV\335\251\244\262Q\326\324\256'\016\316\272\010;R\274RZP:\023\233\342J\026Q\261\323{\356_\250D\371\"\245\034\3633s77\340\233\234o|\235\304\226\321K\251\317\326\331\017/\024\230\220\214\040\011j\300?\003\233\301\040\251p\322o\321\004+-\032\306\034Pzx\027\336\250\300\023\017\033|\274\370\230U\220!\204\004\271\001A<~Bb\262\035\241\315r\351'T\2262D*\231\214W\223n\362)e\2574\3335\331\244WG\373M\252\325:\243\222\222\312\343\376z\226\015s\316\3026\005\372RGU\251\355\244#\336\204\215Y++\2258_\222\"B}\203\356\306\354!\020.n\315Gry\243a\262\355^\244=\240\031\017<[\242\255\351\"\0103S\030\342:j:\200\347\277\006\235(\035>\333\0053\215\256\347$}\343h\300\353\325b\263,Z@\3774\233P\374,\253>nW\244\037\351\361\007\034y\315x\300\233\031\031\264/\335\364S:I\024+\277t\\\344\205(\200\366\012\350\211\363\243\312\275u\225\363*\316\217*2Y\366\252\327k\363\234\002\230\004\026O\372\033\337\230H\375\247\367\224\273\343s\007|o\340\000\321\243R\313Dgb\321\002\344\373\237\334\017x1\353\2150$v\200*\246\267\275\371\361\2037Y\2717B\031\371\322\350\346_\\\375\362zr\263\311o\"\233R\323q\237\225\256c\340P\356]\233\301\247Z?G7\005!\015=\"\361e&u\032[h\346\255\222\177V\016X\370\"5\353\346\005r{\004?\237Y/,yW\203\"j\317'\374F\375\206\226W\223]\352\212\251x\344\276\261N\032\263U\206=O,Z\375\021\370\337\346#0\276\365\260\233#Q<\220\006\327X\021f\307\357\275\000\265\243\354J\014\257\317\"\024|\212\331\011\375|EH\376\310rIrxzt\275\200\027\315%\200'\331\036N\264\226B\210\260`~a{\377G\325\340\202\332Q\263\277\314\251zC\316\233\253\371\325\040\241\311?\037@\326\011L\255\236\222\203\303b6O\262\214\\\370\223\011M\367\307/\261\030\035v\325/\370Yf^>\200\336\241\223&\225\264o\030s\222u\253\234\020\332,A5\177\326\201\303NF\215\035\372Ld\251P\371\354\213iMh\352\223\010\347&\2776cK$p\226z\026\001\027\222\231N`5)\246\033\341r\"\251.@\011\360\312R\357\003qE\364\035\2771(\314\335\202\361\312\232\332+\263\212\236e\251\335\025F9\270I\2173\314}\311x\317<\211R\370)]\247\343\351\241\374E\231?\302O\205_\0248\226\201\037\223\224\037\325(\204#\0329iWu\330\032q\267\343\002_HK)\030BV\021\340\203F\374\232\242><\255\031\002D\343\311\204\3345}I\355\320|\235+9(\311*\343@\226\215\234\213]n\335\212\314\247\347PN\007P\025\371<\243\305\024\244\001'\252\305g\303\003\221\307Aee\304\356\361\003\024\345\177\040\211=\031\275\347\024\340\372\204\377y\340\247\372\302Iu%\221\\\311\364$\200\012\221\277\005\201\251FTC\223?C\022\312\313\3355-\274\355\334\016|X\362U`\017\301;\017\234\272U\304sc\377\026\313S\304q\253\222\315NYG\362\341U\2742\032\271\356\212\014\345!)\313^0\247;C\236\361t\312\262V5\232A\342\250\351\260\250Pm\232\342\031\244\022\371&\321\247\242F\354Mx\025\220\245*\006\015\034o\237:K\261#q*\305\246\222\373\344\253\353\221O\030E\017\010,s&\343\032\351\017\322,VUW\034\027\232\212F\307BC\\\262\177Z\\\315\204%\216s\3265gE\263YBh>\270\273zx\245J\322c)\320B\205\347u\313\310\316\223\247\026\262\313\357\256U\344\307\222\036\206\312\317\353\226\227?\222\243\312\030m\341\037x\\\217-Z\365r\263>\376\270M\323\030\001\371\262!\233\010\241f\261\030\001\250\306\233\277K4\342\201\275J\301F\256\362\254\031tw\223\010!\347*~zp\256q\216x\234f\352\351\013\366'\2567e\360\375\224&\326\236\252\301\347$\306\024\220\315\320\316-\330r\336\223\377\373``c[S%f\2015\210\303T\204+\210\367\2610V\214\0319\353\305OcWD\000\015%n\365\317\013\333\253\366h\026\240h/6B\235\0302Y\033\246\321\317\263\025\363\255\340_\366\260\034\274&t\"\017Rb4C\264Xr\343\311\373\034\367)\320\003x\334\004\034\022\204\021\027;*\004\235\024^\350\007\205b\362\261\345v\312\375\364\013\227\177kzM\353.r\340\240O\227s0\023Z\2724'\246\323\003\230\210\355<\200>?\237\312\235\364\304l\322\023\205\367\017n\350\251\006\276\021\221\011$\026\273\006\207]\231A\023\220\376\333\010\370\342\266\005\334\333\223\3634\332V\"\3339s{\004G\367+l\036=\177\204\276\301\240\007J}\367\302)\3132\253\344\321\025$\237m\303\024\214y\272\225\205\220\272I\347\357\200Ui,J\0227\242C\005)_\231\260{\276(\274,J\316\277\257\346\317~e-8\036\015*\277\360\240h\014\030\354\001\231:\215&\204>\312\313\"f\321+\305\040\364r\005[\311\317\011\330j\360\267JN\213\333\321\311\321#%\027\214\177$\202E!\002i!\310\231\035\257\206\343\253\214?\321KW\011h\243-Zv\314\203W\323\323\234\322\003\256X\331\241\001\254\027\313\3466\301\241\027KQ\333\026\234\273\010\274m\013\316\3757\360\2075Bs\263\243\234\034\233;c=\257\204\353^\247\263D\376a<\275Z\254\322\3655)\205E\215\206\316\226#Gc\307.2V\035no'\304\211\304\263e\003\011\327\3642!}\230\354\262\230M\344\177\000\036^%\244\026\271v\257#\021\006rq\031\301Y\235\024\235\320\300|d\351\236\247\323t\375\205\245\333\312t\252\355iF\226\366\331\260\023AH\355\335h|\005\230x?\265\306\267\377\000!:O\236<\323\324;\364\020\015\324\363\031\376K?\012\221f\303\317\215\207\374\273\346\314X\270\211R:\003\350\014\356\203\007\233\362\342\262\021\177\377\364\331\357\367\236>\333{\366\273\341\263\337\357?\375\335\376o\377\367\317`@\204\331D&S$\346\036\302xF\211r\323\373?oT\326W3*y\326x(\012\355F\222\224\337\040\332\224\322|\234\023=0\372\201\263\242%\031\375\206B\222-\003%*!\321HE\323\337+\235\360A\017`\010\335D+<\346\177\\\316\340\275$\376\366\347\275og{\337N\206\337\276\331\377\366\355\376\267\203?\307!\375C\346\343%\2251\347\272\253\216\302.0\040\275\"\232f\357\226PR\012%\332\033\210\363\275\274q\344D\360l\001\362\272\272\344\317\035D\0036X\367X\015l\017\327DK\352k\216'\250\022\343\007\307\226Qz\331`\317O\264\302\256\343>Z\334V\346\223\022\245\337\177\210\260\313\040!@\304\023P\371\361U\353r\265\240\330\201\347\317\243G\337<:\210n\220\\\317\354\001\013xci\271\344XS\353\275\027\360$\260^5\2365\017|C`\246A\023I\205\276\314\026\233L\317,\364\007\320\266\305\036\252\374\331\210p'>\276\217Er\310\036\262inf\233)QA\237\022\305\233\257(\255\366\360/\263i\376-w\337\213lnu\263|\1778Y%\227Y\213\001\271O\223I\272\"\023f\260\236\034o\326\250\217\274\255\"\374\326\236N\0134\276Q\373\006\355\240\177\253=T\204\020\220\247\205V\307\221\234\311\351\004ET\375x\036\375\364\366\010\354\004\263\361\232\345\177\230.\026\037\263h\232~L\242\366|\375(\213\350\206\311ZI\246\323\212p\213\315\260\003\232H\241I\232\276{\014\336O\217\277\213\264\024\357\0329\035|\023!@\236\025\377\263\251<\300\241\223\006!\230\327.\006\210\314\240\026=A\364\246\011\270W5\230\301\026\222W%Y\0345\275\235\040\303\2628\233\374\251_H\235\343\312\224'\032\312\003\316\003+&\201\012^\203\311\370j\261\032@\241\326\3054\031k\340\266\011\271\012\331\277\027\341\306\363X\272Y\361x\346\3534\255=\222\370A\335\301\241\203\322(\012\204A\256\353\344\337T7\205\305\307\341\202\207\370\225K\265\304\357\015U\006\012\272\327d\010K\244\345\310\243\005\231\021\334\376e\206\213\326\363\347\3668\363\360\000\362@j\231\252\244\261\241\222o\353|u\333\306Ko\254\336\254\340\021\2232\015\031HS\005\020\032if!i\242\302\303\255R\266\305\276\333&.Z\236|\017\233\246\222\324\232\200\341\253Vj\265\211NU!\227J\273\271\353P\276\206\341\010Q\254g\2101;\035/\311\305\232{\017)\036Y\276\225'\265I\346.\031p\310u$\241\237h\216\253\274`\306\321%\273\222\\a}!@\227\205l\312\016\003Z\222\234/\204\216\364\323\224%\314\373\266\317^C\013\225\010?\354\232])s\376i6}\007\264\300\202q\261X&B\206\010z\234*o\371WY{S\345\235\323\301\007\230\"\340\011\241\\\232\226f\213\006\016\264-\3555\331\322\3167kr\026\211\271\263x\206a\206\233\236\272\024O\016/\011\250\206\366\325\276df\252,\347n{\012\331\253D\236\366\235\203\2660\015\373\350\000\032\221wR\274>\237\300H\300\371\361\301\360eW\321\261\2051#\223\274\3512\246Ne\007;\242R\373\311\247d\245\251N\274\021)c\034G.;\231\253\252\261x\234\025\350m\236\324\302\357\370\362\034$]@\347r\264\\A\314\216u\232dp\210_\323h\373c\026\210Y\363\305\024\375s=\316T\243\024\304_\266a5\261K\216\304\022[7q\361=F\256\031vA\260\2048v\362_b\374}O\357Y\261\304\342K\306'\264\032]\2750\202%\314fM_>\234\320"_buf, - "~\256\271\351+\212\256\335\003dm\331\346#\0227\003\022\021\346\363\263x%\225!?\215H\216\360E\316\375\323\011\271\263\025\252\223\376B\346\277\266H\251\220\342TA\366+VQ^\027\230\032\317\276d\353d\266\267\330\254\311:nq\032\277\220/\244\261\263\206~\354\040;\036\331(\270w-E\224\365\223\317\024)\247\037\365q6D+\333\330\310\247\025/\033\337\216Xt\203\370/yO\\\313\277\311\263\013=\223\211\275K\256%\266\257\035\305\3747\225\303R\\\217?%Q\362\313\370\0022\205\300C\251\000\324\356\301\273\320\30559oBP|\2104\001\232I\246\224S\211\322u\226L/[\321\020\356\222\202\300l\374\205Q\177\2727\217\346\244l2\021\337\212y\304\261\312\252\324b\346\310\271U\344\221\343\260g\271\257V\213\305\232\377L\372\3401N\216[P\016p\343\021\215\177\010\326#\236t\234=\341\320\255<\377$U\205\015+\377=G\223\353J@&*[\2474\006\324Z\204d1E\030\230\311\277$\026\361\325tq>\236\342\251\225)E\311\007\0264K\243i\241,S\325\253DO\242\270E\301\356H\277\320\211\315\207Bj\000\354\300\273\3120\005\256\221\234\024fJ(\310[-mH-\220\302]\011\231a\231\364\223\015N1g\375\305\225EQ\241\225\341\316\373\312\313!\031\033!\230eL8yQ\212\014\300\243\357\000\30287g\346\003\231\273td\023\204\377\365/\205\362\003UXP\246\241E\211B\264\316\376J\247u\212\215\213\214\325\020\262\332\254\0073Z\225\357\244z\317\207\234\355c\350sC\250\0333Vu\005y\212\225VR(\254!7\301\247\332\375}\036\324\262\233\254\311\035A\314\341\364\362\0136u\215\370\027QSgM\363F\025\017U\351$\031Owi\350\233t\315\3231G\347d\257\231/6W\327\000Q\237\245\263\364\"\272\2021\177d\274RQ\003$D\325Y\222\251h\3441\001\332d\277\"\033R\364\007\032\373]\030d\243\317\224\017\271\250,x\356\332\350\257\331\307t\011[\346_u24\231U\306r\220\263\203\016\244\035\247\241|\306K8\247\221S\343:\231~i\371;\025zi\223\321\231\262\231\253W\315\\)\026v\341\206\252M\264\302\306\372\325\026d3\340\230\351?1\031\364\303NM6\001\0255PF@\353Y\313\240\037&\340\215\347\214J\273|_!O\177\347*?3\362\235\206\355\302\352^5\317w\274\307\005\323f\000\246\315\272{2\212&5\337\366)O\272\022\273\232\321wE\312\040\265\357\2448=M\307\224\227\014\221\245%o\270\222\350(R\373\242\204exl0u\017\226\203|E&&\260\330\355\0236\225\373c\343\315\233\206ao\330\302w\241IcKd&\255\224O\263\230u\271\3204M\040T\211\017\320\242Hn\300\322\311\027-\025\0349v+e\332\264\264\216\333\307\334\3553\276\301\2757\021;\326\234\332\231\310\255\0336\233\214\245B\364\013l\246\317\014N\210\211\025\324\263>\007eZ,\223q\261d\346EKw\213p\371\275r\223\352fg\253C\243,\204\276\225\242{\265H\272\271\213&\315k\030\007\033\374\024E\326\262B\001N\0304\335^\237\335\221tQ\374\036X[\244\212\224\034\270D\206S$\325a\371\274\205:m#\001\241V\240D\026B\255\246\031M\330<\316VK2\250\267\301\227iP+\277}\266@\2741\326d\336\326\016\271A\216\211H\372z\307!\305\030\245\"e!\312\3218\005\260\014\203fjZ\204\0275\350\213\224\200\317\215\224\200Q\323\325\351R6@Kd~1\232\3435CF\342\311\372,\013\231\2353sO\306\360c%\037\002\033\2162\212\351\265\205\350\005\345@\023\271P\2248\250\261\000,\332\040\215\323\224\034\237a\373\260\302\032m\350\304#Q\2638p\031?)\317}lC\244N\306\204\366f\226p\320\010\255\224\2542\376i\372y\374\205\3747`\023\307\254(\327\\\322s>Y2\227d\007\235_$\241\260\031\371\230\210H>\236L\216\270\034\215\350\320\210\343r\262^=|\230K\252a\013\362\006H\036O\262W\256T\255\031*L\336\2016Y\304\000*y\012\362\210\015\371=\226\224\214s\016\305\365\366b<''\353\361\222b7\243g\344\303x\232\177\215\2250(y\235\347r\233\362_\225\262\330\260h0\250\202\040}49)j\020\225\203\325Qz\354T\002L\361jX\007\352\204\215\\\236y>\024Ib\304\0037Y\377\360c\262:_d\351\372\313\013\013\243\301fI\377\232\210\222i\222a8*\204\342?E\230k\317|\010ED\321h\320\332\365+R5o>\027\367\345\211k\334%\304\227\275\0278o\312\010\273,\313\303\353\255\352iv\205@\32555\332\302\231\306\271\3666;\240r\351\370\246\256nB\242\234F\2260\247\021\013\016j\344\325\301;\210g\326)\272\005\341\304\243L\260\2048r/8\312\336\2045\247\300z\241\221?\317\225\037k\236\365\210\020\032?\337,\010\245\020\330\031\034\274c\213\360y\256\374~k\275\301\245\320\271\005\367\205\263>Y\024\277\361\366\003\217\330\030\331B6\3265\3039\237\204\207\262v\314o\275d\271(\215\256\301\017A\256\256\245\237j\036u\203\275\302\3137\346!\265o\374\355\257\206\201\255\251\365>\300\252\273\365e\340\256\226\326\207A^o\253\371*\367\202SH\343\335u=M\327C\345GH\314~\351\021\264\356\266\033\354\025^\276\326\207\324\366\235\377\215\034\031\021\226$\243\270\032\335F/\040Bh\374|=\021J\341&O\2011\274N\304q\231\371\262\247D\223\222\013k\216h\313\323\307G\347\233\313K\260\250\346\017\202\024\254.\254\300\024\277\355\352\331\252\030n\303\224\021\330\257r8x\030\366\027\015\254\253-`k\274\253\363;\014\322\343\301\250m\373\352\343$\260\224\023r\264\376[Z|\234\273\312)p\3519\352\006\352\334j\240\365\032\325\256\013\177\356W\274\241\350u\327\236[\025\301^\347\326\353E\254{6_g\375\233\240\303\227\324\015z\274c9\216\360-\235\275\244\006\024\234\002O^\216\272>\005\300\201\016w{\342(\270\206\2374\354ud?\036\244\211i\366v3]\247v\313\220b8,itUM\2526\303+X\221\271\3115\310\375}\223]\233\277NR\372\030\264\377\333\247\277{\326\004)\373\013f5\245a_2\012\377\356\375trt\3309\034\036\375\314\241,\023\232K\207>.{\034K\031An\247%<\276\217\032s\223\001\247\352\247\005\010\031\236\230\015\340=<\353P\023\376;\231C;&\271g\263is\346oz\305\030J\177\204y\203\356\333\"t\243\216\2403\030\241\206\356\005\252\247\306\250\344\374Y\315\361\223Oi\245\017\376-w\202\232\367N\207\031(\365\252\272}B\330P\346\3669Y\\P\303Vl\015\\\2503\034\254\277L\223\354:\001\030\303\245}\345!\016\303\262\276R\2502\230K\361P\243\307\313\025\347\225\274\204f\271'#\274\343\202\276SO\201D8\346\344TZ\360\253\366\362dV\205\247\037\265\232\015\323\255\264)\330\330\253\264\305\022y\036'\346\225!\324\362`ti`\000|T\305\313m\314\344\271\002\236T\306\364\321\3406J\005\313\323%\035o6N*1\215\235\362\244\201x\362R\215\024\033\250\333@D\264$\005"_buf, - "\002\30640\322F\0365_\254\230\020f\302\377\005qi\211\354>-QSo\263\315\207\005m\262\016\334+\342\274\307\310+.\202\215Lh\234@\214g\310|\256fI\362e\321\360X\207\320\371C+\306^\375\301g\004\356\011\350ljE\263\221'\213KNQ\313^am\247`\034\334T\206\330\314\275\031\370\213\205\257\362\244\330\310\200\206\250-\375\354%\001~\023r]\370\273\235\361\310\325*\006\307\330r\362Jy,\364\346\201\346,Q\306!\261\315\236\277\365\005$\006\301\342\330\317\372?\231g\233\025\370\205\260`\351\336\355s\033;[`\226K\233\355\214i\017\376\261\233,\327\327O\236\320\250\276M#\272\0062\263\270\234azC\231\\\246\357\204\016\3232\006X\256\"\217\261)\245\245\377\015\334\2079\022\241\026?\322;\312\325\006'R\326\270\025\014|\015\312\216{K\031r\305\254\321\250\222\342\253\220\330\011\034$\031!\361\261h\326\017\360\222\2459\000\013\023#\271%\375\3002\261\323H8!\220&\255\203Tl\023\002\322\362\241\233\036>\324\333k\203\245\341{,\020\211\243\246\363j&c\231d\300\024\002\264\223\"\236\332\304\266\365\263[N^\253NQ\203\034Sk\310\271\234\237\377\266\313\273\254\265\\1C\354\030\223\025L\341I\016\034,\334\275\311Mz1OT\301p\210f\230\242-j\004\236\321X\307\025\210V\372\003$\361A\201\2045\340a\015u\215#\370\214z\370<<^\245W\351|<\265\267\226c\376\202\020\2778\017\001\374,\305\303\200\212Fx\0303\360\314\235OZ\255\226\320jbZ\363,N\020\022\203(\255dL]r\351\027\350\362b\306\224t:(\353p`\231e\274|\214\255\344\022#\214sC\373\264\200\254Z\353\231\241I<\010\364J\336\024h\217\0405\377\303\372\006wm(\277\213UhA\200tV\227\012\300\260r\205\315\321\252\204\330\204{\250\237'\311\234\016\311:\231W\341\352u\201\261\314\025\346\235\362\353\236\037\334p\271\357+{\263\023\264\271\355\370Dr\032\374\235\327\245*\017\243!I\320\255\217\235\320\316\275=\365\266\204\336\225\234./\226\015\357S\262\032O\305\361\333\230C\211u\213g\321\202\344\234\362FN\213\000br\344!\013-\236,\302O+Q\203n\373i\322\370\333\210\333l\0357v\213\214\206\377\263.$\342\040\215\036\\\360\365u\023f\033\252\366\274\035\224\344\327\376d\275\355\344T\003/\271\216\236j\3507\323\001\212\220dN\231\265\333i\002G\\7\347\040q\335\"\375\332\373\000\013\200\34741\353\275\311\036\243P'i\214v\230\2334.\232\335\213\333*Z\317\342\277\215\321v\213\266\343[)~3qU\360CX\316e\007\240\0010P\307\335\343\375\250s\235\\|\324\012\266\306\347\013j\034\242\017\251\343\013r\250\277\270X\254&\344\0279\270@\240\242\337\011\272\311%bA\031\031\2511}\357{L+4\263\227\244P\373\036\222\210\322\017$\235k\3772\375F_\320\342\355\373N\317\000\262}\327\351\211K\352\35394%J\204\030\237K\255\263\212\330\232\220\014\337V\274\314\355\254\014-G\371\366\353\302Ap\273\261\365\022\276\3035\241\313\262\355\212\260\323\253\267\317\266Y\015\025}~\224\265\203\370\325X\234}\014\250\203zq\214s_\0221\363\335=$\207\013\362-p\323\371\005q\341\211\036\202y\334\224R\347\233\215g\313)\035\017(\337\342\177z\204%\003\250V\313\177\360T\204\023\352X\344e\2435\213_c\314\307\363\005\3343\350\3713x\360\2306\015s9\362M\266Y2\236\373&\230\2103\251RnA\325\326r\221\316\327-\232UV\3078\030t\246\213\317d_\040E'\026b\264\300\350\034J\004\222\334,\227n\222\264@)\222\027\251\205\024\275\203L\300%uD#a\220n\011P/\266\216'\177\315'\343\325\244\233|J\331|\2526\012\006\235\332\206\304\244\\\357\370\230\364\353\031,\223n\275#\267\330\254\247\251\031\245\026\031\260U:&};zw\330\357\036\277\033(EFg\375\303\316q\267G\177|\040~\355\036\217\372\307\303\321\331\2407z\007r6A\320\001WyQ\347\273\316\223'\321\273t\376?\337Gg\363\364\002\342\276}\006\221#H+\366%\242{\303N\362\013\004r\212\342\016\244\271^\363\002\015\372,\271\272\272\330\215>C\012\252\321:z\014\177\177z\377A\376\345\375\007\230\376,I\224\311\331`\005Du\362<\301\025\243\315\250\025i\263D\376\013\026\274r\040^\016Z\253\315\274\301\353CE8\331\303\214\240\270\007\"G1\010\344\217\343\363\277\201\355\366S\262\327\011\025\206\255oI$2\224\017\330\040\265O;|\201\261p\366\375A{CfdB\246g\226\234\000\240\3461\241\277\200\024&\357\337\233\037\311\205lq\361\201tt\272\376\240f\010\343m\\%W\340\230\260\242/\352\311\372zQ\344\217\0001s\220\217\253K\032\320\214\307\217\233\242k\\\362\277\247\302NV\244#4\201x\337\2578j\210\366\257\3207J\007\027\353\311X\026r&\300\007\350b8|{r4:\356\037\375\334,V\337Q\373\264-\257>:\307\273\275W\207}\"\3647\2205\005-\026\300\255{8\200\246\3032\201\177@\372\303K\226r\217\373\026\300\311`\235^\222]\006`\032\234\010\3406\346\324\241\202R#\323\346b\265`n\025\020\232$\375\205\024\241\221\246\0303T\215\234\234\222\006\3744j\037\035\021\001\031\335\\!\377\351\354\360\264\327\210\000\020\320\214\016\373D'\367\333G#\366u\330\033\014\001\316,\027\205\324\215l\374\331\255\260\233fK\032\324d1\207\354\356\253\031\004c\035\215~l\223\341~=\030\215\2106GY\216^\265\217\006e\030\263\012\001\354\243\1779\212\274\"Wt\032\016]\027\322\"\345\360\315)Q\20761\371\307\030\253S[G1r\2436a\004\210\036\262K\0134\004@.lR\321\362\261\205R\254\021\011\021\025x\373d|w8|#\244\234\001E\032\323\010\227o0<%K\207\374\320\263\012\012\344\202\272\221\363*\204\364\255B\316\371t\340iP.\040\322\363\376&z\232\307?W\031\014\243\301T\375\350\2471\275\261\226\266\022\025\007\002Y&y\377x\304?\307h\265\012\363\\\223\203\310\326\371c\210&\240\005\235\374:\013@\337m\222\3439\267\322\271\227\030%\030\256\211\244\342\345\244\250\246\222\020Y{\335\321"_buf, - "\341+\213\250\360!\326\313\326\336[\204j\317\336]\354Sl\226\257\177\324\372\307\020\2002|\330X\371\032\307\015\342z\001\240\004\312\206\314pm/\361l&r\225\372;o\373=E\243SF\211!\022\243\233\213\302b\333\255\305\040V\266S\353\333c\024Q\352\335a0\322[\016\015\276\325D%\367\032\261\006K\3554J\245\355\026A\015\303\325\036\322K\016>(R\231X\257\026K\235X\252\0114\040\234\375dZR\040\271b\230Hb\367fr\204\016\274&\357a\377\3251\005\343#;\026\375\024\027\177\200X\331\225\241\010\316\372\203\316\361\011\354gvZZ\231\030\371\325B\375]\373\264o!\372v\360:'\005\305\3649(CA\313.+D\222N\373dxf\275\215\345_\215\226\037\376\351\214\034\307\332o{\215\213\361\222\334\234\223Us7\237\206\254V\274\353\334\240`\277$E\235\307\040\366\331\265\267\344TFo{\3037\307]%\261\200\225\256\265\206\213\025+N6\036Y\364\0315ZXX9k\270\357a\334h)\325{\265\231SL\236\205\227RE\257\341\3425\350u\206\207\307}\313(\344_]$\272?\223\231p\330\361\2202J\271H:\016Z\362\012\301\316W\030ly\333\2131\360q^\032t\241\360\213CY\321J\036\264\006g\235N\257\327\015\020\221\227D\345;\376\270\345\266\247\310\324\356\037\367\245il_\346\315\302\252\254(\373\341i\273{\010\023\206Ty;\370\261\003\246\245\223\323\343No08>5\264\301[0+\367\002\224\213Y\320\255f\364\362\243\301\341\353`\342\254pI\006\341\012-\244j%\346L\356\312\002\030\325\203\204\040\203\333=\353\014\303\307\020\251P\215Q\310\230Z*UdXz\214\203Hl%L\3111\017&\243\011E\037Q*\256\336\336O'\355~\227\320#\324\032\245\227\366v\213\273$odv\324\271\366\313J\343\233,u\253\206j\362\271\347\316\366\232#L*\237Z\251G\261T\224\245\324\274\252\256w\252JW}\236m\247\226\266\223\327?\357\274On\247g\375\341\341[2<\303\366\220\234u\371\005\3308\250)_s\363\234\034\225Sd\217\215\024!vY\231o\024\301\016\242\002\356\220\037\001\277q\036\024\025\376\2525Z\343\377\240!\021\202;\336\203F\254\022\217\2334\353\271\207?\242\357]\275\240\275\025Vm\014\372\376\247K\226C%\342\227\335\356\036\215\273\024\303\003\351\247d\236\002p\212f\245X&\253Lg\334\351\365\333\247\207\307*3I]\304\203\213d>^\245\213\375(v7\201S\252\347\022\033\314\366\365\341\217=r\017\203\2504\274\343}W\266\030\012\275NI\317\354\263\3344\254\256q\346\357j\264\275t\333\363It\345\243\373\356\215&n\230\274\321\273k\277\274*m?]\020\370\263\207\356\260\242\274\303\000y\207%\345e\261T\346\023\225nI\370\267*F\376\255A\346h3\372\313\016\322\302\242\214\303\2044\"\377tF\355\021\231\322\2437\360\307\313\021\371\"\376\040JG\253\373\2727\034=\203I-\353\246\335]\274\340\367HAkKF\355\356\217\355~\207(0\206Vw\267\251(\275M\343(\247R\3506\206\343\260\340-D\222Z\005\314\001\2619Xy\016\341\020\245V\311\3377\351\212\006\271\004\005\235\367K\020J\243*>C\232\320%\240\031w\014\312(\007\307\250\015\210Q\007\004\3436\300\027\265\301.\276&\340\342\226\240\026\267\015\262\010\177\371*\215\257\250\023Y\021\200\251\250\021M\021\214\243\370J\010\2120\354D\335\250\211p\274D\375H\211p\214\304\327DG\224\302E\324\217\210\330\036\013q\253(\210\232\360\017\367\002\371p+\230\207{\210v(\211s\370\332\010\207\322\330\206\332Q\015\025\360\014w\216d\360c\030P|A\025\334\202\033\261\340\305*\3342J\241^|\202\033\231\260-&\341N\320\010w\202C\270\033\004\302V\330\203ZQ\007^\274\301\355#\015\0021\006_\001]\020\202+\270ED\201\020\343\216\240\004\267\000\"\270E\370\300W\002\016|e\310\300\035\200\005\356\020&pO\000\002\367\005\032\240\213sD\266\225\360\001\326J\313\257W>\322\345{\333_\337\365\014v\367\200\207\257\007u\270\237\040\207\373\015o\370\372\300\206\373\003i\370u\200\031~%0\206J\0326L(\235TI\3365\017fI\375\\\013\304\243*\270c\327\304u\370\320\034\267\201\343\310yZJh{\230\253\265\036\370\206K\372\040\310F\376o\365\325\375\264\335\037\320\321\357\375\324\351\235\260[_\226^\315\307`\007@6sO\371r\320\020\035\024R\016\016rg@\220\234c\375\030\220\372\321\037\365\343>\352G|\324\217\365\370\352(\217\377\004|\307\257\014\331\261\201\344\243\302V\322M\326\343t\272\277\337^.W\213_\016\244\210/\230\015y\347>G\371\340\3774\040\360T\263\3614(@\207T:(R\206\217\270\367\321-\220\000\376,\2465\357\256\243AH\254k\214\313`\035\001\305L\351\033]\344\251\336SXy7\347\221\320\321\213\274=J\000\255\243\036zB\234\372=M\326\037\216\335M\256<\343\002\236a\353\232o\225^I\365\331V\213k\266\334\363\267\3419\035\244P\220z\345\033\353pG\366.\026\364\001\317VX~\233\363/C\361X&\227\267vI\350S\030<-\263\227\237\3236=\20586\314\310\3301\207d\307\034\220\377\037\301\241\264\036\337\336[\026\312\377\372V\301\227\3276d\352\323V\220\327\255\303\217\326\306\305|x\262\312\243>\004Y'O\350KM\255\343\364U\335G\215\2268\266\253\032^\203\266d\267\365\013\221\301\037\267\250\334\245\243i\240H\025\274\307\266t\031v\232Nk\247m\035\332\332\235\205K\2328\267\340\177\207\276\242\316\205u\347\236\243[Ks\273O,\301K\356\376\270\225\326\040\362\177\365\304\275\325\023uy\362\335\375\261\262\222\341\327\265\024\253\213\266[\360v{\014\372\274\376\034\336{\036\007<\207\037\235\307\025n\247\204\217\251\365\224\353x\227\220\216\274w\357\205e\023\333'\251\333\245\311N\257\202\375\"\310V\266\235\335\242&\013\331\326N5\330\355\307s\2112\015a\226B\212\001\314n\375B\354^N\263\027b\360\262\265\"\300\322U\331\306\345\267nmi\331\272c\233\326mX\263\354v,\347\302\256\327~\345\266A\005\232\252d#\025\246r\025\253\024\3228\343\234u\247\333\363=3B\2250?U3\000\342\355\037\223\266\305/\336\205\031\013\307;\3321\213(\366\320\012\037DQ\200V\040\237\037\346%\303[\341\272\366\040\302\263H\355\350\231\332VI2\037\237O\223\021\317J\227\265\256w\260\334q\014T\303~<\354t\200Gz\261\274\340\346\252\214Z\252\362\342\014\032\306*\301?h\342;\370\300\366f\255\030\236\237\216\026\247\315$;z\201N$L_\367\317:BDN\3425\2210$\301\035\326z\271\200\224\244\213f\265\177"_buf, - "w\374\362\350\254\3279{\331\033\020\025\326\177M\356\300\207\375\316\321Y\267\307\247\333\233\223\023\361Kwg\347\377\001t+\311(>\367\011\000"_buf}; - -} - -std::string_view dds::detail::catch2_embedded_single_header_str() noexcept { - static const std::string decompressed = [] { - neo::string_dynbuf_io str; - neo::gzip_decompress(str, catch2_gzip_bufs); - str.shrink_uncommitted(); - return std::move(str.string()); - }(); - return decompressed; -} diff --git a/src/dds/catch2_embedded.hpp b/src/dds/catch2_embedded.hpp deleted file mode 100644 index 2132ba36..00000000 --- a/src/dds/catch2_embedded.hpp +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include - -namespace dds::detail { - -std::string_view catch2_embedded_single_header_str() noexcept; - -} // namespace dds::detail diff --git a/src/dds/cli/cmd/build.cpp b/src/dds/cli/cmd/build.cpp deleted file mode 100644 index f8e36a50..00000000 --- a/src/dds/cli/cmd/build.cpp +++ /dev/null @@ -1,41 +0,0 @@ -#include "../options.hpp" - -#include "./build_common.hpp" - -#include -#include -#include -#include -#include - -using namespace dds; - -namespace dds::cli::cmd { - -int build(const options& opts) { - if (!opts.build.add_repos.empty()) { - auto cat = opts.open_pkg_db(); - for (auto& str : opts.build.add_repos) { - auto repo = pkg_remote::connect(str); - repo.store(cat.database()); - } - } - - if (opts.build.update_repos || !opts.build.add_repos.empty()) { - update_all_remotes(opts.open_pkg_db().database()); - } - - auto builder = create_project_builder(opts); - builder.build({ - .out_root = opts.out_path.value_or(fs::current_path() / "_build"), - .existing_lm_index = opts.build.lm_index, - .emit_lmi = {}, - .tweaks_dir = opts.build.tweaks_dir, - .toolchain = opts.load_toolchain(), - .parallel_jobs = opts.jobs, - }); - - return 0; -} - -} // namespace dds::cli::cmd diff --git a/src/dds/cli/cmd/build_common.cpp b/src/dds/cli/cmd/build_common.cpp deleted file mode 100644 index 74ff51d9..00000000 --- a/src/dds/cli/cmd/build_common.cpp +++ /dev/null @@ -1,45 +0,0 @@ -#include "./build_common.hpp" - -#include -#include -#include - -using namespace dds; - -builder dds::cli::create_project_builder(const dds::cli::options& opts) { - sdist_build_params main_params = { - .subdir = "", - .build_tests = opts.build.want_tests, - .run_tests = opts.build.want_tests, - .build_apps = opts.build.want_apps, - .enable_warnings = !opts.disable_warnings, - }; - - auto man - = value_or(package_manifest::load_from_directory(opts.project_dir), package_manifest{}); - auto cat_path = opts.pkg_db_dir.value_or(pkg_db::default_path()); - auto repo_path = opts.pkg_cache_dir.value_or(pkg_cache::default_local_path()); - - builder builder; - if (!opts.build.lm_index.has_value()) { - auto cat = pkg_db::open(cat_path); - // Build the dependencies - pkg_cache::with_cache( // - repo_path, - pkg_cache_flags::write_lock | pkg_cache_flags::create_if_absent, - [&](pkg_cache repo) { - // Download dependencies - auto deps = repo.solve(man.dependencies, cat); - get_all(deps, repo, cat); - for (const pkg_id& pk : deps) { - auto sdist_ptr = repo.find(pk); - assert(sdist_ptr); - sdist_build_params deps_params; - deps_params.subdir = fs::path("_deps") / sdist_ptr->manifest.id.to_string(); - builder.add(*sdist_ptr, deps_params); - } - }); - } - builder.add(sdist{std::move(man), opts.project_dir}, main_params); - return builder; -} diff --git a/src/dds/cli/cmd/build_common.hpp b/src/dds/cli/cmd/build_common.hpp deleted file mode 100644 index eaa4dc33..00000000 --- a/src/dds/cli/cmd/build_common.hpp +++ /dev/null @@ -1,11 +0,0 @@ -#include "../options.hpp" - -#include - -#include - -namespace dds::cli { - -dds::builder create_project_builder(const options& opts); - -} // namespace dds::cli diff --git a/src/dds/cli/cmd/build_deps.cpp b/src/dds/cli/cmd/build_deps.cpp deleted file mode 100644 index 59fe1d12..00000000 --- a/src/dds/cli/cmd/build_deps.cpp +++ /dev/null @@ -1,65 +0,0 @@ -#include "../options.hpp" - -#include -#include -#include -#include - -#include -#include -#include -#include - -namespace dds::cli::cmd { - -int build_deps(const options& opts) { - dds::build_params params{ - .out_root = opts.out_path.value_or(fs::current_path() / "_deps"), - .existing_lm_index = {}, - .emit_lmi = opts.build.lm_index.value_or("INDEX.lmi"), - .emit_cmake = opts.build_deps.cmake_file, - .tweaks_dir = opts.build.tweaks_dir, - .toolchain = opts.load_toolchain(), - .parallel_jobs = opts.jobs, - }; - - dds::builder bd; - dds::sdist_build_params sdist_params; - - auto all_file_deps = opts.build_deps.deps_files // - | ranges::views::transform([&](auto dep_fpath) { - dds_log(info, "Reading deps from {}", dep_fpath.string()); - return dds::dependency_manifest::from_file(dep_fpath).dependencies; - }) - | ranges::actions::join; - - auto cmd_deps = ranges::views::transform(opts.build_deps.deps, [&](auto dep_str) { - return dds::dependency::parse_depends_string(dep_str); - }); - - auto all_deps = ranges::views::concat(all_file_deps, cmd_deps) | ranges::to_vector; - - auto cat = opts.open_pkg_db(); - dds::pkg_cache::with_cache( // - opts.pkg_cache_dir.value_or(pkg_cache::default_local_path()), - dds::pkg_cache_flags::write_lock | dds::pkg_cache_flags::create_if_absent, - [&](dds::pkg_cache repo) { - // Download dependencies - dds_log(info, "Loading {} dependencies", all_deps.size()); - auto deps = repo.solve(all_deps, cat); - dds::get_all(deps, repo, cat); - for (const dds::pkg_id& pk : deps) { - auto sdist_ptr = repo.find(pk); - assert(sdist_ptr); - dds::sdist_build_params deps_params; - deps_params.subdir = sdist_ptr->manifest.id.to_string(); - dds_log(info, "Dependency: {}", sdist_ptr->manifest.id.to_string()); - bd.add(*sdist_ptr, deps_params); - } - }); - - bd.build(params); - return 0; -} - -} // namespace dds::cli::cmd diff --git a/src/dds/cli/cmd/compile_file.cpp b/src/dds/cli/cmd/compile_file.cpp deleted file mode 100644 index bfc045af..00000000 --- a/src/dds/cli/cmd/compile_file.cpp +++ /dev/null @@ -1,21 +0,0 @@ -#include "../options.hpp" - -#include "./build_common.hpp" - -namespace dds::cli::cmd { - -int compile_file(const options& opts) { - auto builder = create_project_builder(opts); - builder.compile_files(opts.compile_file.files, - { - .out_root = opts.out_path.value_or(fs::current_path() / "_build"), - .existing_lm_index = opts.build.lm_index, - .emit_lmi = {}, - .tweaks_dir = opts.build.tweaks_dir, - .toolchain = opts.load_toolchain(), - .parallel_jobs = opts.jobs, - }); - return 0; -} - -} // namespace dds::cli::cmd diff --git a/src/dds/cli/cmd/pkg_create.cpp b/src/dds/cli/cmd/pkg_create.cpp deleted file mode 100644 index 004f7916..00000000 --- a/src/dds/cli/cmd/pkg_create.cpp +++ /dev/null @@ -1,55 +0,0 @@ -#include "../options.hpp" - -#include -#include - -#include -#include -#include -#include - -using namespace fansi::literals; - -namespace dds::cli::cmd { - -int pkg_create(const options& opts) { - dds::sdist_params params{ - .project_dir = opts.project_dir, - .dest_path = {}, - .force = opts.if_exists == if_exists::replace, - .include_apps = true, - .include_tests = true, - }; - return boost::leaf::try_catch( - [&] { - auto pkg_man = package_manifest::load_from_directory(params.project_dir).value(); - auto default_filename = fmt::format("{}.tar.gz", pkg_man.id.to_string()); - auto filepath = opts.out_path.value_or(fs::current_path() / default_filename); - create_sdist_targz(filepath, params); - dds_log(info, - "Created source dirtribution archive: .bold.cyan[{}]"_styled, - filepath.string()); - return 0; - }, - [&](boost::leaf::bad_result, e_missing_file missing, e_human_message msg) { - dds_log( - error, - "A required file is missing for creating a source distribution for [.bold.yellow[{}]]"_styled, - params.project_dir.string()); - dds_log(error, "Error: .bold.yellow[{}]"_styled, msg.value); - dds_log(error, "Missing file: .bold.red[{}]"_styled, missing.path.string()); - write_error_marker("no-package-json5"); - return 1; - }, - [&](std::error_code ec, e_human_message msg, boost::leaf::e_file_name file) { - dds_log(error, "Error: .bold.red[{}]"_styled, msg.value); - dds_log(error, - "Failed to access file [.bold.red[{}]]: .br.yellow[{}]"_styled, - file.value, - ec.message()); - write_error_marker("failed-package-json5-scan"); - return 1; - }); -} - -} // namespace dds::cli::cmd diff --git a/src/dds/cli/cmd/pkg_get.cpp b/src/dds/cli/cmd/pkg_get.cpp deleted file mode 100644 index 8ef66929..00000000 --- a/src/dds/cli/cmd/pkg_get.cpp +++ /dev/null @@ -1,73 +0,0 @@ -#include "../options.hpp" - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -namespace dds::cli::cmd { - -static int _pkg_get(const options& opts) { - auto cat = opts.open_pkg_db(); - for (const auto& item : opts.pkg.get.pkgs) { - auto id = pkg_id::parse(item); - auto info = *cat.get(id); - auto tsd = get_package_sdist(info); - auto dest = opts.out_path.value_or(fs::current_path()) / id.to_string(); - dds_log(info, "Create sdist at {}", dest.string()); - fs::remove_all(dest); - safe_rename(tsd.sdist.path, dest); - } - return 0; -} - -int pkg_get(const options& opts) { - return boost::leaf::try_catch( // - [&] { - try { - return _pkg_get(opts); - } catch (...) { - dds::capture_exception(); - } - }, - [&](neo::url_validation_error url_err, dds::e_url_string bad_url) { - dds_log(error, - "Invalid package URL in the database [{}]: {}", - bad_url.value, - url_err.what()); - return 1; - }, - [&](const json5::parse_error& e, neo::url bad_url) { - dds_log(error, - "Error parsing JSON5 document package downloaded from [{}]: {}", - bad_url.to_string(), - e.what()); - return 1; - }, - [](dds::e_sqlite3_error_exc e) { - dds_log(error, "Error accessing the package database: {}", e.message); - return 1; - }, - [](e_nonesuch nonesuch) -> int { - nonesuch.log_error("There is no entry in the package database for '{}'."); - write_error_marker("pkg-get-no-pkg-id-listing"); - return 1; - }, - [&](dds::e_system_error_exc e, dds::network_origin conn) { - dds_log(error, - "Error opening connection to [{}:{}]: {}", - conn.hostname, - conn.port, - e.message); - return 1; - }); -} - -} // namespace dds::cli::cmd diff --git a/src/dds/cli/cmd/pkg_import.cpp b/src/dds/cli/cmd/pkg_import.cpp deleted file mode 100644 index a2f2362e..00000000 --- a/src/dds/cli/cmd/pkg_import.cpp +++ /dev/null @@ -1,81 +0,0 @@ -#include "../options.hpp" - -#include -#include -#include - -#include -#include -#include -#include -#include - -#include -#include - -using namespace fansi::literals; - -namespace dds::cli::cmd { - -struct e_importing { - std::string value; -}; - -static int _pkg_import(const options& opts) { - return pkg_cache::with_cache( // - opts.pkg_cache_dir.value_or(pkg_cache::default_local_path()), - pkg_cache_flags::write_lock | pkg_cache_flags::create_if_absent, - [&](auto repo) { - // Lambda to import an sdist object - auto import_sdist - = [&](const sdist& sd) { repo.import_sdist(sd, dds::if_exists(opts.if_exists)); }; - - for (std::string_view sdist_where : opts.pkg.import.items) { - DDS_E_SCOPE(e_importing{std::string(sdist_where)}); - neo_assertion_breadcrumbs("Importing sdist", sdist_where); - if (sdist_where.starts_with("http://") || sdist_where.starts_with("https://")) { - auto tmp_sd = download_expand_sdist_targz(sdist_where); - import_sdist(tmp_sd.sdist); - } else if (fs::is_directory(sdist_where)) { - auto sd = sdist::from_directory(sdist_where); - import_sdist(sd); - } else { - auto tmp_sd = expand_sdist_targz(sdist_where); - import_sdist(tmp_sd.sdist); - } - } - if (opts.pkg.import.from_stdin) { - auto tmp_sd = dds::expand_sdist_from_istream(std::cin, ""); - repo.import_sdist(tmp_sd.sdist, dds::if_exists(opts.if_exists)); - } - return 0; - }); -} - -int pkg_import(const options& opts) { - return boost::leaf::try_catch( - [&] { - try { - return _pkg_import(opts); - } catch (...) { - dds::capture_exception(); - } - }, - [&](const json5::parse_error& e) { - dds_log(error, "Error parsing JSON in package archive: {}", e.what()); - return 1; - }, - [](dds::e_sqlite3_error_exc e) { - dds_log(error, "Unexpected database error: {}", e.message); - return 1; - }, - [](e_system_error_exc err, e_importing what) { - dds_log( - error, - "Error while importing source distribution from [.bold.red[{}]]: .br.yellow[{}]"_styled, - what.value, - err.message); - return 1; - }); -} -} // namespace dds::cli::cmd diff --git a/src/dds/cli/cmd/pkg_ls.cpp b/src/dds/cli/cmd/pkg_ls.cpp deleted file mode 100644 index 63234220..00000000 --- a/src/dds/cli/cmd/pkg_ls.cpp +++ /dev/null @@ -1,60 +0,0 @@ -#include "../options.hpp" - -#include -#include -#include - -#include -#include -#include -#include -#include - -#include -#include - -namespace dds::cli::cmd { -static int _pkg_ls(const options& opts) { - auto list_contents = [&](pkg_cache repo) { - auto same_name - = [](auto&& a, auto&& b) { return a.manifest.id.name == b.manifest.id.name; }; - - auto all = repo.iter_sdists(); - auto grp_by_name = all // - | ranges::views::group_by(same_name) // - | ranges::views::transform(ranges::to_vector) // - | ranges::views::transform([](auto&& grp) { - assert(grp.size() > 0); - return std::pair(grp[0].manifest.id.name, grp); - }); - - for (const auto& [name, grp] : grp_by_name) { - dds_log(info, "{}:", name); - for (const dds::sdist& sd : grp) { - dds_log(info, " - {}", sd.manifest.id.version.to_string()); - } - } - - return 0; - }; - - return dds::pkg_cache::with_cache(opts.pkg_cache_dir.value_or(pkg_cache::default_local_path()), - dds::pkg_cache_flags::read, - list_contents); -} - -int pkg_ls(const options& opts) { - return boost::leaf::try_catch( - [&] { - try { - return _pkg_ls(opts); - } catch (...) { - dds::capture_exception(); - } - }, - [](dds::e_sqlite3_error_exc e) { - dds_log(error, "Unexpected database error: {}", e.message); - return 1; - }); -} -} // namespace dds::cli::cmd diff --git a/src/dds/cli/cmd/pkg_repo_add.cpp b/src/dds/cli/cmd/pkg_repo_add.cpp deleted file mode 100644 index 1f0eabe9..00000000 --- a/src/dds/cli/cmd/pkg_repo_add.cpp +++ /dev/null @@ -1,24 +0,0 @@ -#include "../options.hpp" - -#include "./pkg_repo_err_handle.hpp" - -#include -#include - -namespace dds::cli::cmd { - -static int _pkg_repo_add(const options& opts) { - auto cat = opts.open_pkg_db(); - auto repo = pkg_remote::connect(opts.pkg.repo.add.url); - repo.store(cat.database()); - if (opts.pkg.repo.add.update) { - repo.update_pkg_db(cat.database()); - } - return 0; -} - -int pkg_repo_add(const options& opts) { - return handle_pkg_repo_remote_errors([&] { return _pkg_repo_add(opts); }); -} - -} // namespace dds::cli::cmd diff --git a/src/dds/cli/cmd/pkg_repo_err_handle.cpp b/src/dds/cli/cmd/pkg_repo_err_handle.cpp deleted file mode 100644 index 58e59356..00000000 --- a/src/dds/cli/cmd/pkg_repo_err_handle.cpp +++ /dev/null @@ -1,75 +0,0 @@ -#include "./pkg_repo_err_handle.hpp" - -#include "../options.hpp" - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -using namespace fansi::literals; - -int dds::cli::cmd::handle_pkg_repo_remote_errors(std::function fn) { - return boost::leaf::try_catch( - [&] { - try { - return fn(); - } catch (...) { - dds::capture_exception(); - } - }, - [](neo::url_validation_error url_err, neo::url bad_url) { - dds_log(error, "Invalid URL [{}]: {}", bad_url.to_string(), url_err.what()); - return 1; - }, - [](dds::http_status_error err, dds::http_response_info resp, neo::url bad_url) { - dds_log(error, - "An HTTP error occured while requesting [{}]: HTTP Status {} {}", - err.what(), - bad_url.to_string(), - resp.status, - resp.status_message); - return 1; - }, - [](const json5::parse_error& e, neo::url bad_url) { - dds_log(error, - "Error parsing JSON downloaded from URL [.br.red[{}]`]: {}"_styled, - bad_url.to_string(), - e.what()); - return 1; - }, - [](dds::e_sqlite3_error_exc e, neo::url url) { - dds_log(error, - "Error accessing remote database [.br.red[{}]`]: {}"_styled, - url.to_string(), - e.message); - return 1; - }, - [](dds::e_sqlite3_error_exc e) { - dds_log(error, "Unexpected database error: {}", e.message); - return 1; - }, - [](dds::e_system_error_exc e, dds::network_origin conn) { - dds_log(error, - "Error communicating with [.br.red[{}://{}:{}]`]: {}"_styled, - conn.protocol, - conn.hostname, - conn.port, - e.message); - return 1; - }, - [](matchv, e_nonesuch missing) { - missing.log_error( - "Cannot delete remote '.br.red[{}]', as no such remote repository is locally registered by that name."_styled); - write_error_marker("repo-rm-no-such-repo"); - return 1; - }); -} diff --git a/src/dds/cli/cmd/pkg_repo_err_handle.hpp b/src/dds/cli/cmd/pkg_repo_err_handle.hpp deleted file mode 100644 index ff5d731e..00000000 --- a/src/dds/cli/cmd/pkg_repo_err_handle.hpp +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include - -namespace dds::cli::cmd { - -int handle_pkg_repo_remote_errors(std::function); - -} // namespace dds::cli::cmd \ No newline at end of file diff --git a/src/dds/cli/cmd/pkg_repo_ls.cpp b/src/dds/cli/cmd/pkg_repo_ls.cpp deleted file mode 100644 index a94f3f68..00000000 --- a/src/dds/cli/cmd/pkg_repo_ls.cpp +++ /dev/null @@ -1,33 +0,0 @@ -#include "../options.hpp" - -#include "./pkg_repo_err_handle.hpp" - -#include -#include - -#include - -namespace dds::cli::cmd { - -static int _pkg_repo_ls(const options& opts) { - auto pkg_db = opts.open_pkg_db(); - neo::sqlite3::database_ref db = pkg_db.database(); - - auto st = db.prepare("SELECT name, url, db_mtime FROM dds_pkg_remotes"); - auto tups = neo::sqlite3::iter_tuples>(st); - for (auto [name, remote_url, mtime] : tups) { - fmt::print("Remote '{}':\n", name); - fmt::print(" Updates URL: {}\n", remote_url); - if (mtime) { - fmt::print(" Last Modified: {}\n", *mtime); - } - fmt::print("\n"); - } - return 0; -} - -int pkg_repo_ls(const options& opts) { - return handle_pkg_repo_remote_errors([&] { return _pkg_repo_ls(opts); }); -} - -} // namespace dds::cli::cmd diff --git a/src/dds/cli/cmd/pkg_repo_remove.cpp b/src/dds/cli/cmd/pkg_repo_remove.cpp deleted file mode 100644 index 82560b0b..00000000 --- a/src/dds/cli/cmd/pkg_repo_remove.cpp +++ /dev/null @@ -1,26 +0,0 @@ -#include "../options.hpp" - -#include "./pkg_repo_err_handle.hpp" - -#include -#include -#include - -namespace dds::cli::cmd { - -static int _pkg_repo_remove(const options& opts) { - auto cat = opts.open_pkg_db(); - for (auto&& rm_name : opts.pkg.repo.remove.names) { - dds::remove_remote(cat, rm_name); - } - return 0; -} - -int pkg_repo_remove(const options& opts) { - return handle_pkg_repo_remote_errors([&] { - DDS_E_SCOPE(opts.pkg.repo.subcommand); - return _pkg_repo_remove(opts); - }); -} - -} // namespace dds::cli::cmd diff --git a/src/dds/cli/cmd/pkg_repo_update.cpp b/src/dds/cli/cmd/pkg_repo_update.cpp deleted file mode 100644 index eb4ea64a..00000000 --- a/src/dds/cli/cmd/pkg_repo_update.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include "../options.hpp" - -#include "./pkg_repo_err_handle.hpp" - -#include -#include - -namespace dds::cli::cmd { - -static int _pkg_repo_update(const options& opts) { - update_all_remotes(opts.open_pkg_db().database()); - return 0; -} - -int pkg_repo_update(const options& opts) { - return handle_pkg_repo_remote_errors([&] { return _pkg_repo_update(opts); }); -} - -} // namespace dds::cli::cmd diff --git a/src/dds/cli/cmd/pkg_search.cpp b/src/dds/cli/cmd/pkg_search.cpp deleted file mode 100644 index 27159b94..00000000 --- a/src/dds/cli/cmd/pkg_search.cpp +++ /dev/null @@ -1,60 +0,0 @@ -#include "../options.hpp" - -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -using namespace fansi::literals; - -namespace dds::cli::cmd { - -static int _pkg_search(const options& opts) { - auto cat = opts.open_pkg_db(); - auto results = *dds::pkg_search(cat.database(), opts.pkg.search.pattern); - for (pkg_group_search_result const& found : results.found) { - fmt::print( - " Name: .bold[{}]\n" - "Versions: .bold[{}]\n" - " From: .bold[{}]\n" - " .bold[{}]\n\n"_styled, - found.name, - joinstr(", ", found.versions | ranges::views::transform(&semver::version::to_string)), - found.remote_name, - found.description); - } - - if (results.found.empty()) { - dds_log(error, - "There are no packages that match the given pattern \".bold.red[{}]\""_styled, - opts.pkg.search.pattern.value_or("*")); - write_error_marker("pkg-search-no-result"); - return 1; - } - return 0; -} - -int pkg_search(const options& opts) { - return boost::leaf::try_catch( - [&] { - try { - return _pkg_search(opts); - } catch (...) { - capture_exception(); - } - }, - [](e_nonesuch missing) { - missing.log_error( - "There are no packages that match the given pattern \".bold.red[{}]\""_styled); - write_error_marker("pkg-search-no-result"); - return 1; - }); -} - -} // namespace dds::cli::cmd diff --git a/src/dds/cli/cmd/repoman_add.cpp b/src/dds/cli/cmd/repoman_add.cpp deleted file mode 100644 index dae04030..00000000 --- a/src/dds/cli/cmd/repoman_add.cpp +++ /dev/null @@ -1,71 +0,0 @@ -#include "../options.hpp" - -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -namespace dds::cli::cmd { - -static int _repoman_add(const options& opts) { - auto rpkg = any_remote_pkg::from_url(neo::url::parse(opts.repoman.add.url_str)); - auto temp_sdist = get_package_sdist(rpkg); - - dds::pkg_listing add_info{ - .ident = temp_sdist.sdist.manifest.id, - .deps = temp_sdist.sdist.manifest.dependencies, - .description = opts.repoman.add.description, - .remote_pkg = rpkg, - }; - - auto repo = repo_manager::open(opts.repoman.repo_dir); - repo.add_pkg(add_info, opts.repoman.add.url_str); - return 0; -} - -int repoman_add(const options& opts) { - return boost::leaf::try_catch( // - [&] { - try { - return _repoman_add(opts); - } catch (...) { - dds::capture_exception(); - } - }, - [](dds::e_sqlite3_error_exc, - boost::leaf::match, - dds::pkg_id pkid) { - dds_log(error, "Package {} is already present in the repository", pkid.to_string()); - write_error_marker("dup-pkg-add"); - return 1; - }, - [](http_status_error, http_response_info resp, neo::url url) { - dds_log(error, - "Error resulted from HTTP request [{}]: {} {}", - url.to_string(), - resp.status, - resp.status_message); - return 1; - }, - [](dds::user_error e, neo::url url) -> int { - dds_log(error, "Invalid URL '{}': {}", url.to_string(), e.what()); - write_error_marker("repoman-add-invalid-pkg-url"); - throw; - }, - [](dds::e_sqlite3_error_exc e, dds::e_repo_import_targz tgz) { - dds_log(error, "Database error while importing tar file {}: {}", tgz.path, e.message); - return 1; - }, - [](dds::e_system_error_exc e, dds::e_open_repo_db db) { - dds_log(error, "Error while opening repository database {}: {}", db.path, e.message); - return 1; - }); -} - -} // namespace dds::cli::cmd diff --git a/src/dds/cli/cmd/repoman_import.cpp b/src/dds/cli/cmd/repoman_import.cpp deleted file mode 100644 index 7f0aec45..00000000 --- a/src/dds/cli/cmd/repoman_import.cpp +++ /dev/null @@ -1,57 +0,0 @@ -#include "../options.hpp" - -#include -#include - -#include -#include -#include - -namespace dds::cli::cmd { - -static int _repoman_import(const options& opts) { - auto repo = repo_manager::open(opts.repoman.repo_dir); - for (auto pkg : opts.repoman.import.files) { - repo.import_targz(pkg); - } - return 0; -} - -int repoman_import(const options& opts) { - return boost::leaf::try_catch( // - [&] { - try { - return _repoman_import(opts); - } catch (...) { - dds::capture_exception(); - } - }, - [](dds::e_sqlite3_error_exc, - boost::leaf::match, - dds::e_repo_import_targz tgz, - dds::pkg_id pkid) { - dds_log(error, - "Package {} (from {}) is already present in the repository", - pkid.to_string(), - tgz.path); - return 1; - }, - [](dds::e_system_error_exc e, dds::e_repo_import_targz tgz) { - dds_log(error, "Failed to import file {}: {}", tgz.path, e.message); - return 1; - }, - [](const std::runtime_error& e, dds::e_repo_import_targz tgz) { - dds_log(error, "Unknown error while importing file {}: {}", tgz.path, e.what()); - return 1; - }, - [](dds::e_sqlite3_error_exc e, dds::e_repo_import_targz tgz) { - dds_log(error, "Database error while importing tar file {}: {}", tgz.path, e.message); - return 1; - }, - [](dds::e_system_error_exc e, dds::e_open_repo_db db) { - dds_log(error, "Error while opening repository database {}: {}", db.path, e.message); - return 1; - }); -} - -} // namespace dds::cli::cmd diff --git a/src/dds/cli/cmd/repoman_init.cpp b/src/dds/cli/cmd/repoman_init.cpp deleted file mode 100644 index 6fa2f2b6..00000000 --- a/src/dds/cli/cmd/repoman_init.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#include "../options.hpp" - -#include -#include -#include - -#include -#include - -namespace dds::cli::cmd { - -static int _repoman_init(const options& opts) { - auto repo = repo_manager::create(opts.repoman.repo_dir, opts.repoman.init.name); - dds_log(info, "Created new repository '{}' in {}", repo.name(), repo.root()); - return 0; -} - -int repoman_init(const options& opts) { - return boost::leaf::try_catch( // - [&] { - try { - return _repoman_init(opts); - } catch (...) { - dds::capture_exception(); - } - }, - [](dds::e_sqlite3_error_exc e, dds::e_init_repo init, dds::e_init_repo_db init_db) { - dds_log(error, - "SQLite error while initializing repository in [{}] (SQlite database {}): {}", - init.path, - init_db.path, - e.message); - return 1; - }, - [](dds::e_system_error_exc e, dds::e_open_repo_db db) { - dds_log(error, "Error while opening repository database {}: {}", db.path, e.message); - return 1; - }, - [](dds::e_sqlite3_error_exc e, dds::e_init_repo init) { - dds_log(error, - "SQLite error while initializing repository in [{}]: {}", - init.path, - e.message); - return 1; - }); -} - -} // namespace dds::cli::cmd diff --git a/src/dds/cli/cmd/repoman_ls.cpp b/src/dds/cli/cmd/repoman_ls.cpp deleted file mode 100644 index 7c88989e..00000000 --- a/src/dds/cli/cmd/repoman_ls.cpp +++ /dev/null @@ -1,37 +0,0 @@ -#include "../options.hpp" - -#include -#include -#include - -#include -#include - -#include - -namespace dds::cli::cmd { - -static int _repoman_ls(const options& opts) { - auto repo = repo_manager::open(opts.repoman.repo_dir); - for (auto id : repo.all_packages()) { - std::cout << id.to_string() << '\n'; - } - return 0; -} - -int repoman_ls(const options& opts) { - return boost::leaf::try_catch( // - [&] { - try { - return _repoman_ls(opts); - } catch (...) { - dds::capture_exception(); - } - }, - [](dds::e_system_error_exc e, dds::e_open_repo_db db) { - dds_log(error, "Error while opening repository database {}: {}", db.path, e.message); - return 1; - }); -} - -} // namespace dds::cli::cmd diff --git a/src/dds/cli/cmd/repoman_remove.cpp b/src/dds/cli/cmd/repoman_remove.cpp deleted file mode 100644 index 29fc4ab6..00000000 --- a/src/dds/cli/cmd/repoman_remove.cpp +++ /dev/null @@ -1,45 +0,0 @@ -#include "../options.hpp" - -#include -#include - -#include -#include -#include - -namespace dds::cli::cmd { - -static int _repoman_remove(const options& opts) { - auto repo = repo_manager::open(opts.repoman.repo_dir); - for (auto& str : opts.repoman.remove.pkgs) { - auto id = dds::pkg_id::parse(str); - repo.delete_package(id); - } - return 0; -} - -int repoman_remove(const options& opts) { - return boost::leaf::try_catch( // - [&] { - try { - return _repoman_remove(opts); - } catch (...) { - dds::capture_exception(); - } - }, - [](dds::e_system_error_exc e, dds::e_repo_delete_path tgz, dds::pkg_id pkid) { - dds_log(error, - "Cannot delete requested package '{}' from repository {}: {}", - pkid.to_string(), - tgz.path, - e.message); - write_error_marker("repoman-rm-no-such-package"); - return 1; - }, - [](dds::e_system_error_exc e, dds::e_open_repo_db db) { - dds_log(error, "Error while opening repository database {}: {}", db.path, e.message); - return 1; - }); -} - -} // namespace dds::cli::cmd diff --git a/src/dds/cli/dispatch_main.cpp b/src/dds/cli/dispatch_main.cpp deleted file mode 100644 index 1be462b3..00000000 --- a/src/dds/cli/dispatch_main.cpp +++ /dev/null @@ -1,105 +0,0 @@ -#include "./dispatch_main.hpp" - -#include "./error_handler.hpp" -#include "./options.hpp" - -#include -#include - -using namespace dds; - -namespace dds::cli { - -namespace cmd { -using command = int(const options&); - -command build_deps; -command build; -command compile_file; -command install_yourself; -command pkg_create; -command pkg_get; -command pkg_import; -command pkg_ls; -command pkg_repo_add; -command pkg_repo_update; -command pkg_repo_ls; -command pkg_repo_remove; -command pkg_search; -command repoman_add; -command repoman_import; -command repoman_init; -command repoman_ls; -command repoman_remove; - -} // namespace cmd - -int dispatch_main(const options& opts) noexcept { - return dds::handle_cli_errors([&] { - DDS_E_SCOPE(opts.subcommand); - switch (opts.subcommand) { - case subcommand::build: - return cmd::build(opts); - case subcommand::pkg: { - DDS_E_SCOPE(opts.pkg.subcommand); - switch (opts.pkg.subcommand) { - case pkg_subcommand::ls: - return cmd::pkg_ls(opts); - case pkg_subcommand::create: - return cmd::pkg_create(opts); - case pkg_subcommand::get: - return cmd::pkg_get(opts); - case pkg_subcommand::import: - return cmd::pkg_import(opts); - case pkg_subcommand::repo: { - DDS_E_SCOPE(opts.pkg.repo.subcommand); - switch (opts.pkg.repo.subcommand) { - case pkg_repo_subcommand::add: - return cmd::pkg_repo_add(opts); - case pkg_repo_subcommand::update: - return cmd::pkg_repo_update(opts); - case pkg_repo_subcommand::ls: - return cmd::pkg_repo_ls(opts); - case pkg_repo_subcommand::remove: - return cmd::pkg_repo_remove(opts); - case pkg_repo_subcommand::_none_:; - } - neo::unreachable(); - } - case pkg_subcommand::search: - return cmd::pkg_search(opts); - case pkg_subcommand::_none_:; - } - neo::unreachable(); - } - case subcommand::repoman: { - DDS_E_SCOPE(opts.repoman.subcommand); - switch (opts.repoman.subcommand) { - case repoman_subcommand::import: - return cmd::repoman_import(opts); - case repoman_subcommand::add: - return cmd::repoman_add(opts); - case repoman_subcommand::init: - return cmd::repoman_init(opts); - case repoman_subcommand::remove: - return cmd::repoman_remove(opts); - case repoman_subcommand::ls: - return cmd::repoman_ls(opts); - case repoman_subcommand::_none_:; - } - neo::unreachable(); - } - case subcommand::compile_file: - return cmd::compile_file(opts); - case subcommand::build_deps: - return cmd::build_deps(opts); - case subcommand::install_yourself: - return cmd::install_yourself(opts); - case subcommand::_none_:; - } - neo::unreachable(); - return 6; - }); -} - -} // namespace dds::cli diff --git a/src/dds/cli/error_handler.cpp b/src/dds/cli/error_handler.cpp deleted file mode 100644 index c7168d81..00000000 --- a/src/dds/cli/error_handler.cpp +++ /dev/null @@ -1,111 +0,0 @@ -#include "./error_handler.hpp" -#include "./options.hpp" - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -using namespace dds; -using namespace fansi::literals; - -namespace { - -auto handlers = std::tuple( // - [](neo::url_validation_error exc, e_url_string bad_url) { - dds_log(error, "Invalid URL '{}': {}", bad_url.value, exc.what()); - return 1; - }, - [](boost::leaf::catch_ exc, - json5::parse_error parse_err, - boost::leaf::e_file_name* maybe_fpath) { - dds_log(error, "{}", exc.value().what()); - dds_log(error, "Invalid JSON5 was found: {}", parse_err.what()); - if (maybe_fpath) { - dds_log(error, " (While reading from [{}])", maybe_fpath->value); - } - dds_log(error, "{}", exc.value().explanation()); - write_error_marker("package-json5-parse-error"); - return 1; - }, - [](user_error exc, matchv) { - write_error_marker("build-failed-test-failed"); - dds_log(error, "{}", exc.what()); - dds_log(error, "{}", exc.explanation()); - dds_log(error, "Refer: {}", exc.error_reference()); - return 1; - }, - [](boost::leaf::catch_ exc) { - dds_log(error, "{}", exc.value().what()); - dds_log(error, "{}", exc.value().explanation()); - dds_log(error, "Refer: {}", exc.value().error_reference()); - return 1; - }, - [](user_cancelled) { - dds_log(critical, "Operation cancelled by the user"); - return 2; - }, - [](e_system_error_exc e, neo::url url, http_response_info) { - dds_log(error, - "An error occured while downloading [.bold.red[{}]]: {}"_styled, - url.to_string(), - e.message); - return 1; - }, - [](e_system_error_exc e, network_origin origin, neo::url* url) { - dds_log(error, - "Network error communicating with .bold.red[{}://{}:{}]: {}"_styled, - origin.protocol, - origin.hostname, - origin.port, - e.message); - if (url) { - dds_log(error, " (While accessing URL [.bold.red[{}]])"_styled, url->to_string()); - } - return 1; - }, - [](e_system_error_exc err, e_loading_toolchain, e_toolchain_file* tc_file) { - dds_log(error, "Failed to load toolchain: .br.yellow[{}]"_styled, err.message); - if (tc_file) { - dds_log(error, " (While loading from file [.bold.red[{}]])"_styled, tc_file->value); - } - return 1; - }, - [](e_system_error_exc exc, boost::leaf::verbose_diagnostic_info const& diag) { - dds_log(critical, - "An unhandled std::system_error arose. THIS IS A DDS BUG! Info: {}", - diag); - dds_log(critical, "Exception message from std::system_error: {}", exc.message); - return 42; - }, - [](boost::leaf::verbose_diagnostic_info const& diag) { - dds_log(critical, "An unhandled error arose. THIS IS A DDS BUG! Info: {}", diag); - return 42; - }); -} // namespace - -int dds::handle_cli_errors(std::function fn) noexcept { - return boost::leaf::try_catch( - [&] { - try { - return fn(); - } catch (...) { - capture_exception(); - } - }, - handlers); -} diff --git a/src/dds/cli/options.cpp b/src/dds/cli/options.cpp deleted file mode 100644 index 189087a0..00000000 --- a/src/dds/cli/options.cpp +++ /dev/null @@ -1,541 +0,0 @@ -#include "./options.hpp" - -#include -#include -#include -#include -#include -#include - -#include -#include - -using namespace dds; -using namespace debate; -using namespace fansi::literals; - -namespace { - -struct setup { - dds::cli::options& opts; - - explicit setup(dds::cli::options& opts) - : opts(opts) {} - - // Util argument common to a lot of operations - argument if_exists_arg{ - .long_spellings = {"if-exists"}, - .help = "What to do if the resource already exists", - .valname = "{replace,skip,fail}", - .action = put_into(opts.if_exists), - }; - - argument if_missing_arg{ - .long_spellings = {"if-missing"}, - .help = "What to do if the resource does not exist", - .valname = "{fail,ignore}", - .action = put_into(opts.if_missing), - }; - - argument toolchain_arg{ - .long_spellings = {"toolchain"}, - .short_spellings = {"t"}, - .help = "The toolchain to use when building", - .valname = "", - .action = put_into(opts.toolchain), - }; - - argument project_arg{ - .long_spellings = {"project"}, - .short_spellings = {"p"}, - .help = "The project to build. If not given, uses the current working directory", - .valname = "", - .action = put_into(opts.project_dir), - }; - - argument no_warn_arg{ - .long_spellings = {"no-warn", "no-warnings"}, - .help = "Disable build warnings", - .nargs = 0, - .action = store_true(opts.disable_warnings), - }; - - argument out_arg{ - .long_spellings = {"out", "output"}, - .short_spellings = {"o"}, - .help = "Path to the output", - .valname = "", - .action = put_into(opts.out_path), - }; - - argument lm_index_arg{ - .long_spellings = {"libman-index"}, - .help = "Path to a libman index to use", - .valname = "", - .action = put_into(opts.build.lm_index), - }; - - argument jobs_arg{ - .long_spellings = {"jobs"}, - .short_spellings = {"j"}, - .help = "Set the maximum number of parallel jobs to execute", - .valname = "", - .action = put_into(opts.jobs), - }; - - argument repoman_repo_dir_arg{ - .help = "The directory of the repository to manage", - .valname = "", - .required = true, - .action = put_into(opts.repoman.repo_dir), - }; - - argument tweaks_dir_arg{ - .long_spellings = {"tweaks-dir"}, - .short_spellings = {"TD"}, - .help - = "Base directory of " - "\x1b]8;;https://vector-of-bool.github.io/2020/10/04/lib-configuration.html\x1b\\tweak " - "headers\x1b]8;;\x1b\\ that should be available to the build.", - .valname = "", - .action = put_into(opts.build.tweaks_dir), - }; - - void do_setup(argument_parser& parser) noexcept { - parser.add_argument({ - .long_spellings = {"log-level"}, - .short_spellings = {"l"}, - .help = "" - "Set the dds logging level. One of 'trace', 'debug', 'info', \n" - "'warn', 'error', 'critical', or 'silent'", - .valname = "", - .action = put_into(opts.log_level), - }); - parser.add_argument({ - .long_spellings = {"data-dir"}, - .help - = "" - "(Advanced) " - "Override dds's data directory. This is used for various caches and databases.\n" - "The default is a user-local directory that differs depending on platform.", - .valname = "", - .action = put_into(opts.data_dir), - }); - parser.add_argument({ - .long_spellings = {"pkg-cache-dir"}, - .help = "(Advanced) Override dds's local package cache directory.", - .valname = "", - .action = put_into(opts.pkg_cache_dir), - }); - parser.add_argument({ - .long_spellings = {"pkg-db-path"}, - .help = "(Advanced) Override dds's default package database path.", - .valname = "", - .action = put_into(opts.pkg_db_dir), - }); - - setup_main_commands(parser.add_subparsers({ - .description = "The operation to perform", - .action = put_into(opts.subcommand), - })); - } - - void setup_main_commands(subparser_group& group) { - setup_build_cmd(group.add_parser({ - .name = "build", - .help = "Build a project", - })); - setup_compile_file_cmd(group.add_parser({ - .name = "compile-file", - .help = "Compile individual files in the project", - })); - setup_build_deps_cmd(group.add_parser({ - .name = "build-deps", - .help = "Build a set of dependencies and generate a libman index", - })); - setup_pkg_cmd(group.add_parser({ - .name = "pkg", - .help = "Manage packages and package remotes", - })); - setup_repoman_cmd(group.add_parser({ - .name = "repoman", - .help = "Manage a dds package repository", - })); - setup_install_yourself_cmd(group.add_parser({ - .name = "install-yourself", - .help = "Have this dds executable install itself onto your PATH", - })); - } - - void setup_build_cmd(argument_parser& build_cmd) { - build_cmd.add_argument(toolchain_arg.dup()); - build_cmd.add_argument(project_arg.dup()); - build_cmd.add_argument({ - .long_spellings = {"no-tests"}, - .help = "Do not build and run project tests", - .nargs = 0, - .action = debate::store_false(opts.build.want_tests), - }); - build_cmd.add_argument({ - .long_spellings = {"no-apps"}, - .help = "Do not build project applications", - .nargs = 0, - .action = debate::store_false(opts.build.want_apps), - }); - build_cmd.add_argument(no_warn_arg.dup()); - build_cmd.add_argument(out_arg.dup()).help = "Directory where dds will write build results"; - - build_cmd.add_argument({ - .long_spellings = {"add-repo"}, - .help = "" - "Add remote repositories to the package database before building\n" - "(Implies --update-repos)", - .valname = "", - .can_repeat = true, - .action = debate::push_back_onto(opts.build.add_repos), - }); - build_cmd.add_argument({ - .long_spellings = {"update-repos"}, - .short_spellings = {"U"}, - .help = "Update package repositories before building", - .nargs = 0, - .action = debate::store_true(opts.build.update_repos), - }); - build_cmd.add_argument(lm_index_arg.dup()).help - = "Path to a libman index file to use for loading project dependencies"; - build_cmd.add_argument(jobs_arg.dup()); - build_cmd.add_argument(tweaks_dir_arg.dup()); - } - - void setup_compile_file_cmd(argument_parser& compile_file_cmd) noexcept { - compile_file_cmd.add_argument(project_arg.dup()); - compile_file_cmd.add_argument(toolchain_arg.dup()); - compile_file_cmd.add_argument(no_warn_arg.dup()).help = "Disable compiler warnings"; - compile_file_cmd.add_argument(jobs_arg.dup()).help - = "Set the maximum number of files to compile in parallel"; - compile_file_cmd.add_argument(lm_index_arg.dup()); - compile_file_cmd.add_argument(out_arg.dup()); - compile_file_cmd.add_argument(tweaks_dir_arg.dup()); - compile_file_cmd.add_argument({ - .help = "One or more source files to compile", - .valname = "", - .can_repeat = true, - .action = debate::push_back_onto(opts.compile_file.files), - }); - } - - void setup_build_deps_cmd(argument_parser& build_deps_cmd) noexcept { - build_deps_cmd.add_argument(toolchain_arg.dup()).required; - build_deps_cmd.add_argument(jobs_arg.dup()); - build_deps_cmd.add_argument(out_arg.dup()); - build_deps_cmd.add_argument(lm_index_arg.dup()).help - = "Destination path for the generated libman index file"; - build_deps_cmd.add_argument({ - .long_spellings = {"deps-file"}, - .short_spellings = {"d"}, - .help = "Path to a JSON5 file listing dependencies", - .valname = "", - .can_repeat = true, - .action = debate::push_back_onto(opts.build_deps.deps_files), - }); - build_deps_cmd.add_argument({ - .long_spellings = {"cmake"}, - .help = "Generate a CMake file at the given path that will create import targets for " - "the dependencies", - .valname = "", - .action = debate::put_into(opts.build_deps.cmake_file), - }); - build_deps_cmd.add_argument(tweaks_dir_arg.dup()); - build_deps_cmd.add_argument({ - .help = "Dependency statement strings", - .valname = "", - .can_repeat = true, - .action = debate::push_back_onto(opts.build_deps.deps), - }); - } - - void setup_pkg_cmd(argument_parser& pkg_cmd) { - auto& pkg_group = pkg_cmd.add_subparsers({ - .valname = "", - .action = put_into(opts.pkg.subcommand), - }); - setup_pkg_init_db_cmd(pkg_group.add_parser({ - .name = "init-db", - .help = "Initialize a new package database file (Path specified with '--pkg-db-path')", - })); - pkg_group.add_parser({ - .name = "ls", - .help = "List locally available packages", - }); - setup_pkg_create_cmd(pkg_group.add_parser({ - .name = "create", - .help = "Create a source distribution archive of a project", - })); - setup_pkg_get_cmd(pkg_group.add_parser({ - .name = "get", - .help = "Obtain a copy of a package from a remote", - })); - setup_pkg_import_cmd(pkg_group.add_parser({ - .name = "import", - .help = "Import a source distribution archive into the local package cache", - })); - setup_pkg_repo_cmd(pkg_group.add_parser({ - .name = "repo", - .help = "Manage package repositories", - })); - setup_pkg_search_cmd(pkg_group.add_parser({ - .name = "search", - .help = "Search for packages available to download", - })); - } - - void setup_pkg_create_cmd(argument_parser& pkg_create_cmd) { - pkg_create_cmd.add_argument(project_arg.dup()).help - = "Path to the project for which to create a source distribution.\n" - "Default is the current working directory."; - pkg_create_cmd.add_argument(out_arg.dup()).help - = "Destination path for the source distributioon archive"; - pkg_create_cmd.add_argument(if_exists_arg.dup()).help - = "What to do if the destination names an existing file"; - } - - void setup_pkg_get_cmd(argument_parser& pkg_get_cmd) { - pkg_get_cmd.add_argument({ - .valname = "", - .can_repeat = true, - .action = push_back_onto(opts.pkg.get.pkgs), - }); - pkg_get_cmd.add_argument(out_arg.dup()).help - = "Directory where obtained packages will be placed.\n" - "Default is the current working directory."; - } - - void setup_pkg_init_db_cmd(argument_parser& pkg_init_db_cmd) { - pkg_init_db_cmd.add_argument(if_exists_arg.dup()).help - = "What to do if the database file already exists"; - } - - void setup_pkg_import_cmd(argument_parser& pkg_import_cmd) noexcept { - pkg_import_cmd.add_argument({ - .long_spellings = {"stdin"}, - .help = "Import a source distribution archive from standard input", - .nargs = 0, - .action = debate::store_true(opts.pkg.import.from_stdin), - }); - pkg_import_cmd.add_argument(if_exists_arg.dup()).help - = "What to do if the package already exists in the local cache"; - pkg_import_cmd.add_argument({ - .help = "One or more paths/URLs to source distribution archives to import", - .valname = "", - .can_repeat = true, - .action = debate::push_back_onto(opts.pkg.import.items), - }); - } - - void setup_pkg_repo_cmd(argument_parser& pkg_repo_cmd) noexcept { - auto& pkg_repo_grp = pkg_repo_cmd.add_subparsers({ - .valname = "", - .action = put_into(opts.pkg.repo.subcommand), - }); - setup_pkg_repo_add_cmd(pkg_repo_grp.add_parser({ - .name = "add", - .help = "Add a package repository", - })); - setup_pkg_repo_remove_cmd(pkg_repo_grp.add_parser({ - .name = "remove", - .help = "Remove one or more package repositories", - })); - - pkg_repo_grp.add_parser({ - .name = "update", - .help = "Update package repository information", - }); - pkg_repo_grp.add_parser({ - .name = "ls", - .help = "List locally registered package repositories", - }); - } - - void setup_pkg_repo_add_cmd(argument_parser& pkg_repo_add_cmd) noexcept { - pkg_repo_add_cmd.add_argument({ - .help = "URL of a repository to add", - .valname = "", - .required = true, - .action = debate::put_into(opts.pkg.repo.add.url), - }); - pkg_repo_add_cmd.add_argument({ - .long_spellings = {"no-update"}, - .help = "Do not immediately update for the new package repository", - .nargs = 0, - .action = debate::store_false(opts.pkg.repo.add.update), - }); - } - - void setup_pkg_repo_remove_cmd(argument_parser& pkg_repo_remove_cmd) noexcept { - pkg_repo_remove_cmd.add_argument({ - .help = "Name of one or more repositories to remove", - .valname = "", - .can_repeat = true, - .action = push_back_onto(opts.pkg.repo.remove.names), - }); - pkg_repo_remove_cmd.add_argument(if_missing_arg.dup()).help - = "What to do if any of the named repositories do not exist"; - } - - void setup_pkg_search_cmd(argument_parser& pkg_repo_search_cmd) noexcept { - pkg_repo_search_cmd.add_argument({ - .help - = "A name or glob-style pattern. Only matching packages will be returned. \n" - "Searching is case-insensitive. Only the .italic[name] will be matched (not the \n" - "version).\n\nIf this parameter is omitted, the search will return .italic[all] \n" - "available packages."_styled, - .valname = "", - .action = put_into(opts.pkg.search.pattern), - }); - } - - void setup_repoman_cmd(argument_parser& repoman_cmd) { - auto& grp = repoman_cmd.add_subparsers({ - .valname = "", - .action = put_into(opts.repoman.subcommand), - }); - - setup_repoman_init_cmd(grp.add_parser({ - .name = "init", - .help = "Initialize a directory as a new repository", - })); - auto& ls_cmd = grp.add_parser({ - .name = "ls", - .help = "List the contents of a package repository directory", - }); - ls_cmd.add_argument(repoman_repo_dir_arg.dup()); - setup_repoman_add_cmd(grp.add_parser({ - .name = "add", - .help = "Add a package listing to the repository by URL", - })); - setup_repoman_import_cmd(grp.add_parser({ - .name = "import", - .help = "Import a source distribution into the repository", - })); - setup_repoman_remove_cmd(grp.add_parser({ - .name = "remove", - .help = "Remove packages from a package repository", - })); - } - - void setup_repoman_init_cmd(argument_parser& repoman_init_cmd) { - repoman_init_cmd.add_argument(repoman_repo_dir_arg.dup()); - repoman_init_cmd.add_argument(if_exists_arg.dup()).help - = "What to do if the directory exists and is already repository"; - repoman_init_cmd.add_argument({ - .long_spellings = {"name"}, - .short_spellings = {"n"}, - .help = "Specifiy the name of the new repository", - .valname = "", - .action = put_into(opts.repoman.init.name), - }); - } - - void setup_repoman_import_cmd(argument_parser& repoman_import_cmd) { - repoman_import_cmd.add_argument(repoman_repo_dir_arg.dup()); - repoman_import_cmd.add_argument({ - .help = "Paths to source distribution archives to import", - .valname = "", - .can_repeat = true, - .action = push_back_onto(opts.repoman.import.files), - }); - } - - void setup_repoman_add_cmd(argument_parser& repoman_add_cmd) { - repoman_add_cmd.add_argument(repoman_repo_dir_arg.dup()); - repoman_add_cmd.add_argument({ - .help = "URL to add to the repository", - .valname = "", - .required = true, - .action = put_into(opts.repoman.add.url_str), - }); - repoman_add_cmd.add_argument({ - .long_spellings = {"description"}, - .short_spellings = {"d"}, - .action = put_into(opts.repoman.add.description), - }); - } - - void setup_repoman_remove_cmd(argument_parser& repoman_remove_cmd) { - repoman_remove_cmd.add_argument(repoman_repo_dir_arg.dup()); - repoman_remove_cmd.add_argument({ - .help = "One or more identifiers of packages to remove", - .valname = "", - .can_repeat = true, - .action = push_back_onto(opts.repoman.remove.pkgs), - }); - } - - void setup_install_yourself_cmd(argument_parser& install_yourself_cmd) { - install_yourself_cmd.add_argument({ - .long_spellings = {"where"}, - .help = "The scope of the installation. For .bold[system], installs in a global \n" - "directory for all users of the system. For .bold[user], installs in a \n" - "user-specific directory for executable binaries."_styled, - .valname = "{user,system}", - .action = put_into(opts.install_yourself.where), - }); - install_yourself_cmd.add_argument({ - .long_spellings = {"dry-run"}, - .help - = "Do not actually perform any operations, but log what .italic[would] happen"_styled, - .nargs = 0, - .action = store_true(opts.dry_run), - }); - install_yourself_cmd.add_argument({ - .long_spellings = {"no-modify-path"}, - .help = "Do not attempt to modify the PATH environment variable", - .nargs = 0, - .action = store_false(opts.install_yourself.fixup_path_env), - }); - install_yourself_cmd.add_argument({ - .long_spellings = {"symlink"}, - .help = "Create a symlink at the installed location to the existing 'dds' executable\n" - "instead of copying the executable file", - .nargs = 0, - .action = store_true(opts.install_yourself.symlink), - }); - } -}; - -} // namespace - -void cli::options::setup_parser(debate::argument_parser& parser) noexcept { - setup{*this}.do_setup(parser); -} - -pkg_db dds::cli::options::open_pkg_db() const { - return pkg_db::open(this->pkg_db_dir.value_or(pkg_db::default_path())); -} - -toolchain dds::cli::options::load_toolchain() const { - if (!toolchain) { - auto def = dds::toolchain::get_default(); - if (!def) { - throw_user_error(); - } - return *def; - } - // Convert the given string to a toolchain - auto& tc_str = *toolchain; - DDS_E_SCOPE(e_loading_toolchain{tc_str}); - if (tc_str.starts_with(":")) { - DDS_E_SCOPE(e_toolchain_builtin{tc_str}); - auto default_tc = tc_str.substr(1); - auto tc = dds::toolchain::get_builtin(default_tc); - if (!tc.has_value()) { - throw_user_error< - errc::invalid_builtin_toolchain>("Invalid built-in toolchain name '{}'", - default_tc); - } - return std::move(*tc); - } else { - DDS_E_SCOPE(e_toolchain_file{tc_str}); - return parse_toolchain_json5(slurp_file(tc_str)); - } -} diff --git a/src/dds/db/database.test.cpp b/src/dds/db/database.test.cpp deleted file mode 100644 index 98fb8695..00000000 --- a/src/dds/db/database.test.cpp +++ /dev/null @@ -1,7 +0,0 @@ -#include - -#include - -using namespace std::literals; - -TEST_CASE("Create a database") { auto db = dds::database::open(":memory:"s); } diff --git a/src/dds/deps.cpp b/src/dds/deps.cpp deleted file mode 100644 index 1f0c5bf4..00000000 --- a/src/dds/deps.cpp +++ /dev/null @@ -1,111 +0,0 @@ -#include "./deps.hpp" - -#include -#include - -#include -#include - -#include - -#include -#include - -using namespace dds; - -dependency dependency::parse_depends_string(std::string_view str) { - auto sep_pos = str.find_first_of("=@^~+"); - if (sep_pos == str.npos) { - throw_user_error("Invalid dependency string '{}'", str); - } - - auto name = str.substr(0, sep_pos); - - if (str[sep_pos] == '@') { - ++sep_pos; - } - auto range_str = str.substr(sep_pos); - - try { - auto rng = semver::range::parse_restricted(range_str); - return dependency{std::string(name), {rng.low(), rng.high()}}; - } catch (const semver::invalid_range&) { - throw_user_error( - "Invalid version range string '{}' in dependency string '{}'", range_str, str); - } -} - -dependency_manifest dependency_manifest::from_file(path_ref fpath) { - auto content = slurp_file(fpath); - auto data = json5::parse_data(content); - - dependency_manifest depman; - using namespace semester::walk_ops; - auto res = walk.try_walk( // - data, - require_type{ - "The root of a dependency manifest must be a JSON object"}, - mapping{ - required_key{ - "depends", - "A 'depends' key is required", - require_type{"'depends' must be an array of strings"}, - for_each{ - require_type{"Each dependency should be a string"}, - put_into(std::back_inserter(depman.dependencies), - [](const std::string& str) { - return dependency::parse_depends_string(str); - }), - }, - }, - }); - - res.throw_if_rejected>(); - - return depman; -} - -namespace { - -std::string iv_string(const pubgrub::interval_set::interval_type& iv) { - if (iv.high == semver::version::max_version()) { - return ">=" + iv.low.to_string(); - } - if (iv.low == semver::version()) { - return "<" + iv.high.to_string(); - } - return iv.low.to_string() + " < " + iv.high.to_string(); -} - -} // namespace - -std::string dependency::to_string() const noexcept { - std::stringstream strm; - strm << name << "@"; - if (versions.num_intervals() == 1) { - auto iv = *versions.iter_intervals().begin(); - if (iv.high == iv.low.next_after()) { - strm << iv.low.to_string(); - return strm.str(); - } - if (iv.low == semver::version() && iv.high == semver::version::max_version()) { - return name; - } - strm << "[" << iv_string(iv) << "]"; - return strm.str(); - } - - strm << "["; - auto iv_it = versions.iter_intervals(); - auto it = iv_it.begin(); - const auto stop = iv_it.end(); - while (it != stop) { - strm << "(" << iv_string(*it) << ")"; - ++it; - if (it != stop) { - strm << " || "; - } - } - strm << "]"; - return strm.str(); -} \ No newline at end of file diff --git a/src/dds/deps.hpp b/src/dds/deps.hpp deleted file mode 100644 index c2473486..00000000 --- a/src/dds/deps.hpp +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include - -#include -#include -#include - -#include - -namespace dds { - -using version_range_set = pubgrub::interval_set; - -struct dependency { - std::string name; - version_range_set versions; - - static dependency parse_depends_string(std::string_view str); - - std::string to_string() const noexcept; -}; - -/** - * Represents a dependency listing file, which is a subset of a package manifest - */ -struct dependency_manifest { - std::vector dependencies; - - static dependency_manifest from_file(path_ref where); -}; - -} // namespace dds diff --git a/src/dds/deps.test.cpp b/src/dds/deps.test.cpp deleted file mode 100644 index 6c6d0453..00000000 --- a/src/dds/deps.test.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include - -#include - -TEST_CASE("Parse dependency strings") { - struct case_ { - std::string depstr; - std::string name; - std::string low; - std::string high; - }; - - auto cur = GENERATE(Catch::Generators::values({ - {"foo@1.2.3", "foo", "1.2.3", "1.2.4"}, - {"foo=1.2.3", "foo", "1.2.3", "1.2.4"}, - {"foo^1.2.3", "foo", "1.2.3", "2.0.0"}, - {"foo~1.2.3", "foo", "1.2.3", "1.3.0"}, - {"foo+1.2.3", "foo", "1.2.3", semver::version::max_version().to_string()}, - })); - - auto dep = dds::dependency::parse_depends_string(cur.depstr); - CHECK(dep.name == cur.name); - CHECK(dep.versions.num_intervals() == 1); - auto ver_iv = *dep.versions.iter_intervals().begin(); - CHECK(ver_iv.low == semver::version::parse(cur.low)); - CHECK(ver_iv.high == semver::version::parse(cur.high)); -} diff --git a/src/dds/dym.test.cpp b/src/dds/dym.test.cpp deleted file mode 100644 index 371f65b7..00000000 --- a/src/dds/dym.test.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include "./dym.hpp" - -#include - -TEST_CASE("Basic string distance") { - CHECK(dds::lev_edit_distance("a", "a") == 0); - CHECK(dds::lev_edit_distance("a", "b") == 1); - CHECK(dds::lev_edit_distance("aa", "a") == 1); -} - -TEST_CASE("Find the 'did-you-mean' candidate") { - auto cand = dds::did_you_mean("food", {"foo", "bar"}); - CHECK(cand == "foo"); - cand = dds::did_you_mean("eatable", {"edible", "tangible"}); - CHECK(cand == "edible"); -} diff --git a/src/dds/error/errors.cpp b/src/dds/error/errors.cpp deleted file mode 100644 index 51b8d727..00000000 --- a/src/dds/error/errors.cpp +++ /dev/null @@ -1,340 +0,0 @@ -#include "./errors.hpp" - -#include -#include - -using namespace dds; - -namespace { - -std::string error_url_prefix = "https://vector-of-bool.github.io/docs/dds/err/"; - -std::string error_url_suffix(dds::errc ec) noexcept { - switch (ec) { - case errc::invalid_builtin_toolchain: - return "invalid-builtin-toolchain.html"; - case errc::no_default_toolchain: - return "no-default-toolchain.html"; - case errc::no_such_catalog_package: - return "no-such-catalog-package.html"; - case errc::git_url_ref_mutual_req: - return "git-url-ref-mutual-req.html"; - case errc::test_failure: - return "test-failure.html"; - case errc::compile_failure: - return "compile-failure.html"; - case errc::archive_failure: - return "archive-failure.html"; - case errc::link_failure: - return "link-failure.html"; - case errc::catalog_too_new: - return "catalog-too-new.html"; - case errc::corrupted_catalog_db: - return "corrupted-catalog-db.html"; - case errc::invalid_catalog_json: - return "invalid-catalog-json.html"; - case errc::no_catalog_remote_info: - return "no-pkg-remote.html"; - case errc::git_clone_failure: - return "git-clone-failure.html"; - case errc::invalid_remote_url: - return "invalid-remote-url.html"; - case errc::http_download_failure: - return "http-failure.html"; - case errc::invalid_repo_transform: - return "invalid-repo-transform.html"; - case errc::sdist_ident_mismatch: - return "sdist-ident-mismatch.html"; - case errc::corrupted_build_db: - return "corrupted-build-db.html"; - case errc::invalid_lib_manifest: - return "invalid-lib-manifest.html"; - case errc::invalid_pkg_manifest: - return "invalid-pkg-manifest.html"; - case errc::invalid_version_range_string: - return "invalid-version-string.html#range"; - case errc::invalid_version_string: - return "invalid-version-string.html"; - case errc::invalid_lib_filesystem: - case errc::invalid_pkg_filesystem: - return "invalid-pkg-filesystem.html"; - case errc::unknown_test_driver: - return "unknown-test-driver.html"; - case errc::invalid_pkg_name: - case errc::invalid_pkg_id: - return "invalid-pkg-ident.html"; - case errc::sdist_exists: - return "sdist-exists.html"; - case errc::dependency_resolve_failure: - return "dep-res-failure.html"; - case errc::dup_lib_name: - return "dup-lib-name.html"; - case errc::unknown_usage_name: - return "unknown-usage.html"; - case errc::template_error: - return "template-error.html"; - case errc::none: - break; - } - assert(false && "Unreachable code path generating error explanation URL"); - std::terminate(); -} - -} // namespace - -std::string dds::error_reference_of(dds::errc ec) noexcept { - return error_url_prefix + error_url_suffix(ec); -} - -std::string_view dds::explanation_of(dds::errc ec) noexcept { - switch (ec) { - case errc::invalid_builtin_toolchain: - return R"( -If you start your toolchain name (The `-t` or `--toolchain` argument) -with a leading colon, dds will interpret it as a reference to a built-in -toolchain. (Toolchain file paths cannot begin with a leading colon). - -These toolchain names are encoded into the dds executable and cannot be -modified. -)"; - case errc::no_default_toolchain: - return R"( -`dds` requires a toolchain to be specified in order to execute the build. `dds` -will not perform a "best-guess" at a default toolchain. You may either pass the -name of a built-in toolchain, or write a "default toolchain" file to one of the -supported filepaths. Refer to the documentation for more information. -)"; - case errc::no_such_catalog_package: - return R"( -The installation of a package was requested, but the given package ID was not -able to be found in the package catalog. Check the spelling and version number. -)"; - case errc::git_url_ref_mutual_req: - return R"( -Creating a Git-based catalog entry requires both a URL to clone from and a Git -reference (tag, branch, commit) to clone. -)"; - case errc::test_failure: - return R"( -One or more of the project's tests failed. The failing tests are listed above, -along with their exit code and output. -)"; - case errc::compile_failure: - return R"( -Source compilation failed. Refer to the compiler output. -)"; - case errc::archive_failure: - return R"( -Creating a static library archive failed, which prevents the associated library -from being used as this archive is the input to the linker for downstream -build targets. - -It is unlikely that regular user action can cause static library archiving to -fail. Refer to the output of the archiving tool. -)"; - case errc::link_failure: - return R"( -Linking a runtime binary file failed. There are a variety of possible causes -for this error. Refer to the documentation for more information. -)"; - case errc::catalog_too_new: - return R"( -The catalog database file contains a schema that will automatically be upgraded -by dds when it is opened/modified. It appears that the given catalog database -has had a migration beyond a version that we support. Has the catalog been -modified by a newer version of dds? -)"; - case errc::corrupted_catalog_db: - return R"( -The catalog database schema doesn't match what dds expects. This indicates that -the database file has been modified in a way that dds cannot automatically fix -and handle. -)"; - case errc::invalid_lib_manifest: - return R"( -A library manifest is malformed Refer to the documentation and above error -message for more details. -)"; - case errc::invalid_pkg_manifest: - return R"( -The package manifest is malformed. Refer to the documentation and above error -message for more details. -)"; - case errc::invalid_catalog_json: - return R"( -The catalog JSON that was provided does not match the format that was expected. -Check the JSON schema and try your submission again. -)"; - case errc::no_catalog_remote_info: - return R"( -There is no package remote with the given name -)"; - case errc::git_clone_failure: - return R"( -dds tried to clone a repository using Git, but the clone operation failed. -There are a variety of possible causes. It is best to check the output from -Git in diagnosing this failure. -)"; - case errc::invalid_remote_url: - return R"(The given package/remote URL is invalid)"; - case errc::http_download_failure: - return R"( -There was a problem when trying to download data from an HTTP server. HTTP 40x -errors indicate problems on the client-side, and HTTP 50x errors indicate that -the server itself encountered an error. -)"; - case errc::invalid_repo_transform: - return R"( -A 'transform' property in a catalog entry contains an invalid transformation. -These cannot and should not be saved to the catalog. -)"; - case errc::sdist_ident_mismatch: - return R"( -We tried to automatically generate a source distribution from a package, but -the name and/or version of the package that was generated does not match what -we expected of it. -)"; - case errc::corrupted_build_db: - return R"( -The local build database file is corrupted. The file is stored in the build -directory as `.dds.db', and is safe to delete to clear the bad data. This is -not a likely error, and if you receive this message frequently, please file a -bug report. -)"; - case errc::invalid_version_range_string: - return R"( -Parsing of a version range string failed. Refer to the documentation for more -information. -)"; - case errc::invalid_version_string: - return R"( -`dds` expects all version numbers to conform to the Semantic Versioning -specification. Refer to the documentation and https://semver.org/ for more -information. -)"; - case errc::invalid_lib_filesystem: - case errc::invalid_pkg_filesystem: - return R"( -`dds` prescribes a specific filesystem structure that must be obeyed by -libraries and packages. Refer to the documentation for an explanation and -reference on these prescriptions. -)"; - case errc::unknown_test_driver: - return R"( -`dds` has a pre-defined set of built-in test drivers, and the one specified is -not recognized. Check the documentation for more information. -)"; - case errc::invalid_pkg_id: - return R"(Package IDs must follow a strict format of @.)"; - case errc::invalid_pkg_name: - return R"(Package names allow a limited set of characters and must not be empty.)"; - case errc::sdist_exists: - return R"( -By default, `dds` will not overwrite source distributions that already exist -(either in the repository or a filesystem path). Such an action could -potentially destroy important data. -)"; - case errc::dependency_resolve_failure: - return R"( -The dependency resolution algorithm failed to resolve the requirements of the -project. The algorithm's explanation should give enough information to infer -why there is no possible solution. You may need to reconsider your dependency -versions to avoid conflicts. -)"; - case errc::dup_lib_name: - return R"( -`dds` cannot build code correctly when there is more than one library that has -claimed the same name. It is possible that the duplicate name appears in a -dependency and is not an issue in your own project. Consult the output to see -which packages are claiming the library name. -)"; - case errc::unknown_usage_name: - return R"( -A `uses` or `links` field for a library specifies a library of an unknown name. -Check your spelling, and check that the package containing the library is -available, either from the `package.json5` or from the `INDEX.lmi` that was used -for the build. -)"; - case errc::template_error: - return R"(dds encountered a problem while rendering a file template and cannot continue.)"; - case errc::none: - break; - } - assert(false && "Unexpected execution path during error explanation. This is a DDS bug"); - std::terminate(); -} - -#define BUG_STRING_SUFFIX " <- (Seeing this text is a `dds` bug. Please report it.)" - -std::string_view dds::default_error_string(dds::errc ec) noexcept { - switch (ec) { - case errc::invalid_builtin_toolchain: - return "The built-in toolchain name is invalid"; - case errc::no_default_toolchain: - return "Unable to find a default toolchain to use for the build"; - case errc::no_such_catalog_package: - return "The catalog has no entry for the given package ID"; - case errc::git_url_ref_mutual_req: - return "Git requires both a URL and a ref to clone"; - case errc::test_failure: - return "One or more tests failed"; - case errc::compile_failure: - return "Source compilation failed."; - case errc::archive_failure: - return "Creating a static library archive failed"; - case errc::link_failure: - return "Linking a runtime binary (executable/shared library/DLL) failed"; - case errc::catalog_too_new: - return "The catalog appears to be from a newer version of dds."; - case errc::corrupted_catalog_db: - return "The catalog database appears to be corrupted or invalid"; - case errc::invalid_catalog_json: - return "The given catalog JSON data is not valid"; - case errc::no_catalog_remote_info: - return "Tne named remote does not exist." BUG_STRING_SUFFIX; - case errc::git_clone_failure: - return "A git-clone operation failed."; - case errc::invalid_remote_url: - return "The given package/remote URL is not valid"; - case errc::http_download_failure: - return "There was an error downloading data from an HTTP server."; - case errc::invalid_repo_transform: - return "A repository filesystem transformation is invalid"; - case errc::sdist_ident_mismatch: - return "The package version of a generated source distribution did not match the " - "version\n that was expected of it"; - case errc::corrupted_build_db: - return "The build database file is corrupted"; - case errc::invalid_lib_manifest: - return "The library manifest is invalid"; - case errc::invalid_pkg_manifest: - return "The package manifest is invalid"; - case errc::invalid_version_range_string: - return "Attempted to parse an invalid version range string." BUG_STRING_SUFFIX; - case errc::invalid_version_string: - return "Attempted to parse an invalid version string." BUG_STRING_SUFFIX; - case errc::invalid_lib_filesystem: - case errc::invalid_pkg_filesystem: - return "The filesystem structure of the package/library is invalid." BUG_STRING_SUFFIX; - case errc::invalid_pkg_id: - return "A package identifier is invalid." BUG_STRING_SUFFIX; - case errc::invalid_pkg_name: - return "A package name is invalid." BUG_STRING_SUFFIX; - case errc::sdist_exists: - return "The source ditsribution already exists at the destination " BUG_STRING_SUFFIX; - case errc::unknown_test_driver: - return "The specified test_driver is not known to `dds`"; - case errc::dependency_resolve_failure: - return "`dds` was unable to find a solution for the package dependencies given."; - case errc::dup_lib_name: - return "More than one library has claimed the same name."; - case errc::unknown_usage_name: - return "A `uses` or `links` field names a library that isn't recognized."; - case errc::template_error: - return "There was an error while rendering a template file." BUG_STRING_SUFFIX; - case errc::none: - break; - } - assert(false && "Unexpected execution path during error message creation. This is a DDS bug"); - std::terminate(); -} diff --git a/src/dds/error/nonesuch.cpp b/src/dds/error/nonesuch.cpp deleted file mode 100644 index 288720fc..00000000 --- a/src/dds/error/nonesuch.cpp +++ /dev/null @@ -1,15 +0,0 @@ -#include "./nonesuch.hpp" - -#include - -#include - -using namespace dds; -using namespace fansi::literals; - -void e_nonesuch::log_error(std::string_view fmt) const noexcept { - dds_log(error, fmt, given); - if (nearest) { - dds_log(error, " (Did you mean '.br.yellow[{}]'?)"_styled, *nearest); - } -} diff --git a/src/dds/error/toolchain.hpp b/src/dds/error/toolchain.hpp deleted file mode 100644 index 6fc30ac1..00000000 --- a/src/dds/error/toolchain.hpp +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include - -namespace dds { - -struct e_loading_toolchain { - std::string value; -}; - -struct e_toolchain_file { - std::string value; -}; - -struct e_toolchain_builtin { - std::string value; -}; - -} // namespace dds \ No newline at end of file diff --git a/src/dds/pkg/cache.cpp b/src/dds/pkg/cache.cpp deleted file mode 100644 index 89602aa5..00000000 --- a/src/dds/pkg/cache.cpp +++ /dev/null @@ -1,144 +0,0 @@ -#include "./cache.hpp" - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -using namespace dds; - -using namespace ranges; - -void pkg_cache::_log_blocking(path_ref dirpath) noexcept { - dds_log(warn, "Another process has the package cache directory locked [{}]", dirpath.string()); - dds_log(warn, "Waiting for cache to be released..."); -} - -void pkg_cache::_init_cache_dir(path_ref dirpath) noexcept { fs::create_directories(dirpath); } - -fs::path pkg_cache::default_local_path() noexcept { return dds_data_dir() / "pkg"; } - -pkg_cache pkg_cache::_open_for_directory(bool writeable, path_ref dirpath) { - auto try_read_sdist = [](path_ref p) -> std::optional { - if (starts_with(p.filename().string(), ".")) { - return std::nullopt; - } - try { - return sdist::from_directory(p); - } catch (const std::runtime_error& e) { - dds_log(error, - "Failed to load source distribution from directory '{}': {}", - p.string(), - e.what()); - return std::nullopt; - } - }; - - auto entries = - // Get the top-level `name-version` dirs - fs::directory_iterator(dirpath) // - | neo::lref // - // Convert each dir into an `sdist` object - | ranges::views::transform(try_read_sdist) // - // Drop items that failed to load - | ranges::views::filter([](auto&& opt) { return opt.has_value(); }) // - | ranges::views::transform([](auto&& opt) { return *opt; }) // - | to(); - - return {writeable, dirpath, std::move(entries)}; -} - -void pkg_cache::import_sdist(const sdist& sd, if_exists ife_action) { - neo_assertion_breadcrumbs("Importing sdist archive", sd.manifest.id.to_string()); - if (!_write_enabled) { - dds_log(critical, - "DDS attempted to write into a cache that wasn't opened with a write-lock. This " - "is a hard bug and should be reported. For the safety and integrity of the local " - "cache, we'll hard-exit immediately."); - std::terminate(); - } - auto sd_dest = _root / sd.manifest.id.to_string(); - if (fs::exists(sd_dest)) { - auto msg = fmt:: - format("Package '{}' (Importing from [{}]) is already available in the local cache", - sd.manifest.id.to_string(), - sd.path.string()); - if (ife_action == if_exists::throw_exc) { - throw_user_error(msg); - } else if (ife_action == if_exists::ignore) { - dds_log(warn, msg); - return; - } else { - dds_log(info, msg + " - Replacing"); - } - } - - // Create a temporary location where we are creating it - auto tmp_copy = sd_dest; - tmp_copy.replace_filename(".tmp-import"); - if (fs::exists(tmp_copy)) { - fs::remove_all(tmp_copy); - } - fs::create_directories(tmp_copy.parent_path()); - - // Re-create an sdist from the given sdist. This will prune non-sdist files, rather than just - // fs::copy_all from the source, which may contain extras. - sdist_params params{ - .project_dir = sd.path, - .dest_path = tmp_copy, - .include_apps = true, - .include_tests = true, - }; - create_sdist_in_dir(tmp_copy, params); - - // Swap out the temporary to the final location - if (fs::exists(sd_dest)) { - fs::remove_all(sd_dest); - } - fs::rename(tmp_copy, sd_dest); - _sdists.insert(sdist::from_directory(sd_dest)); - dds_log(info, "Source distribution for '{}' successfully imported", sd.manifest.id.to_string()); -} - -const sdist* pkg_cache::find(const pkg_id& pkg) const noexcept { - auto found = _sdists.find(pkg); - if (found == _sdists.end()) { - return nullptr; - } - return &*found; -} - -std::vector pkg_cache::solve(const std::vector& deps, - const pkg_db& ctlg) const { - return dds::solve( - deps, - [&](std::string_view name) -> std::vector { - auto mine = ranges::views::all(_sdists) // - | ranges::views::filter( - [&](const sdist& sd) { return sd.manifest.id.name == name; }) - | ranges::views::transform([](const sdist& sd) { return sd.manifest.id; }); - auto avail = ctlg.by_name(name); - auto all = ranges::views::concat(mine, avail) | ranges::to_vector; - ranges::sort(all, std::less{}); - ranges::unique(all, std::less{}); - return all; - }, - [&](const pkg_id& pkg_id) { - auto found = find(pkg_id); - if (found) { - return found->manifest.dependencies; - } - return ctlg.dependencies_of(pkg_id); - }); -} diff --git a/src/dds/pkg/cache.hpp b/src/dds/pkg/cache.hpp deleted file mode 100644 index 17555943..00000000 --- a/src/dds/pkg/cache.hpp +++ /dev/null @@ -1,105 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include - -namespace dds { - -enum pkg_cache_flags { - none = 0b00, - read = none, - create_if_absent = 0b01, - write_lock = 0b10, -}; - -enum class if_exists { - replace, - throw_exc, - ignore, -}; - -inline pkg_cache_flags operator|(pkg_cache_flags a, pkg_cache_flags b) { - return static_cast(int(a) | int(b)); -} - -class pkg_cache { - using sdist_set = std::set; - - bool _write_enabled = false; - fs::path _root; - sdist_set _sdists; - - pkg_cache(bool writeable, path_ref p, sdist_set sds) - : _write_enabled(writeable) - , _root(p) - , _sdists(std::move(sds)) {} - - static void _log_blocking(path_ref dir) noexcept; - static void _init_cache_dir(path_ref dir) noexcept; - static pkg_cache _open_for_directory(bool writeable, path_ref); - -public: - template - static decltype(auto) with_cache(path_ref dirpath, pkg_cache_flags flags, Func&& fn) { - if (!fs::exists(dirpath)) { - if (flags & pkg_cache_flags::create_if_absent) { - _init_cache_dir(dirpath); - } - } - - shared_file_mutex mut{dirpath / ".dds-cache-lock"}; - std::shared_lock shared_lk{mut, std::defer_lock}; - std::unique_lock excl_lk{mut, std::defer_lock}; - - bool writeable = (flags & pkg_cache_flags::write_lock) != pkg_cache_flags::none; - - if (writeable) { - if (!excl_lk.try_lock()) { - _log_blocking(dirpath); - excl_lk.lock(); - } - } else { - if (!shared_lk.try_lock()) { - _log_blocking(dirpath); - shared_lk.lock(); - } - } - - auto cache = _open_for_directory(writeable, dirpath); - return std::invoke(NEO_FWD(fn), std::move(cache)); - } - - static fs::path default_local_path() noexcept; - - void import_sdist(const sdist&, if_exists = if_exists::throw_exc); - - const sdist* find(const pkg_id& pk) const noexcept; - - auto iter_sdists() const noexcept { - class ret { - const sdist_set& s; - - public: - ret(const sdist_set& s) - : s(s) {} - - auto begin() const { return s.cbegin(); } - auto end() const { return s.cend(); } - } r{_sdists}; - return r; - } - - std::vector solve(const std::vector& deps, const pkg_db&) const; -}; - -} // namespace dds \ No newline at end of file diff --git a/src/dds/pkg/db.cpp b/src/dds/pkg/db.cpp deleted file mode 100644 index b4aa9573..00000000 --- a/src/dds/pkg/db.cpp +++ /dev/null @@ -1,394 +0,0 @@ -#include "./db.hpp" - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace dds; - -namespace nsql = neo::sqlite3; -using namespace neo::sqlite3::literals; - -namespace dds { - -void add_init_repo(nsql::database_ref db) noexcept; - -} // namespace dds - -namespace { - -void migrate_repodb_1(nsql::database& db) { - db.exec(R"( - CREATE TABLE dds_cat_pkgs ( - pkg_id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - version TEXT NOT NULL, - git_url TEXT, - git_ref TEXT, - lm_name TEXT, - lm_namespace TEXT, - description TEXT NOT NULL, - UNIQUE(name, version), - CONSTRAINT has_source_info CHECK( - ( - git_url NOT NULL - AND git_ref NOT NULL - ) - = 1 - ), - CONSTRAINT valid_lm_info CHECK( - ( - lm_name NOT NULL - AND lm_namespace NOT NULL - ) - + - ( - lm_name ISNULL - AND lm_namespace ISNULL - ) - = 1 - ) - ); - - CREATE TABLE dds_cat_pkg_deps ( - dep_id INTEGER PRIMARY KEY AUTOINCREMENT, - pkg_id INTEGER NOT NULL REFERENCES dds_cat_pkgs(pkg_id), - dep_name TEXT NOT NULL, - low TEXT NOT NULL, - high TEXT NOT NULL, - UNIQUE(pkg_id, dep_name) - ); - )"); -} - -void migrate_repodb_2(nsql::database& db) { - db.exec(R"( - ALTER TABLE dds_cat_pkgs - ADD COLUMN repo_transform TEXT NOT NULL DEFAULT '[]' - )"); -} - -void migrate_repodb_3(nsql::database& db) { - db.exec(R"( - CREATE TABLE dds_pkg_remotes ( - remote_id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL UNIQUE, - url TEXT NOT NULL, - db_etag TEXT, - db_mtime TEXT - ); - - CREATE TABLE dds_pkgs ( - pkg_id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - version TEXT NOT NULL, - description TEXT NOT NULL, - remote_url TEXT NOT NULL, - remote_id INTEGER - REFERENCES dds_pkg_remotes - ON DELETE CASCADE, - UNIQUE (name, version, remote_id) - ); - - INSERT INTO dds_pkgs(pkg_id, - name, - version, - description, - remote_url) - SELECT pkg_id, - name, - version, - description, - 'git+' || git_url || ( - CASE - WHEN lm_name ISNULL THEN '' - ELSE ('?lm=' || lm_namespace || '/' || lm_name) - END - ) || '#' || git_ref - FROM dds_cat_pkgs; - - CREATE TABLE dds_pkg_deps ( - dep_id INTEGER PRIMARY KEY AUTOINCREMENT, - pkg_id INTEGER - NOT NULL - REFERENCES dds_pkgs(pkg_id) - ON DELETE CASCADE, - dep_name TEXT NOT NULL, - low TEXT NOT NULL, - high TEXT NOT NULL, - UNIQUE(pkg_id, dep_name) - ); - - INSERT INTO dds_pkg_deps SELECT * FROM dds_cat_pkg_deps; - - DROP TABLE dds_cat_pkg_deps; - DROP TABLE dds_cat_pkgs; - )"); -} - -void do_store_pkg(neo::sqlite3::database& db, - neo::sqlite3::statement_cache& st_cache, - const pkg_listing& pkg) { - dds_log(debug, "Recording package {}@{}", pkg.ident.name, pkg.ident.version.to_string()); - auto& store_pkg_st = st_cache(R"( - INSERT OR REPLACE INTO dds_pkgs - (name, version, remote_url, description) - VALUES - (?, ?, ?, ?) - )"_sql); - nsql::exec(store_pkg_st, - pkg.ident.name, - pkg.ident.version.to_string(), - pkg.remote_pkg.to_url_string(), - pkg.description); - - auto db_pkg_id = db.last_insert_rowid(); - auto& new_dep_st = st_cache(R"( - INSERT INTO dds_pkg_deps ( - pkg_id, - dep_name, - low, - high - ) VALUES ( - ?, - ?, - ?, - ? - ) - )"_sql); - for (const auto& dep : pkg.deps) { - new_dep_st.reset(); - assert(dep.versions.num_intervals() == 1); - auto iv_1 = *dep.versions.iter_intervals().begin(); - dds_log(trace, " Depends on: {}", dep.to_string()); - nsql::exec(new_dep_st, db_pkg_id, dep.name, iv_1.low.to_string(), iv_1.high.to_string()); - } -} - -void ensure_migrated(nsql::database& db) { - db.exec(R"( - PRAGMA foreign_keys = 1; - CREATE TABLE IF NOT EXISTS dds_cat_meta AS - WITH init(meta) AS (VALUES ('{"version": 0}')) - SELECT * FROM init; - )"); - nsql::transaction_guard tr{db}; - - auto meta_st = db.prepare("SELECT meta FROM dds_cat_meta"); - auto [meta_json] = nsql::unpack_single(meta_st); - - auto meta = nlohmann::json::parse(meta_json); - if (!meta.is_object()) { - dds_log(critical, "Root of database dds_cat_meta cell should be a JSON object"); - throw_external_error(); - } - - auto version_ = meta["version"]; - if (!version_.is_number_integer()) { - dds_log(critical, "'version' key in dds_cat_meta is not an integer"); - throw_external_error( - "The database metadata is invalid [bad dds_meta.version]"); - } - - constexpr int current_database_version = 3; - - int version = version_; - - if (version > current_database_version) { - dds_log(critical, - "Catalog version is {}, but we only support up to {}", - version, - current_database_version); - throw_external_error(); - } - - if (version < 1) { - dds_log(debug, "Applying pkg_db migration 1"); - migrate_repodb_1(db); - } - if (version < 2) { - dds_log(debug, "Applying pkg_db migration 2"); - migrate_repodb_2(db); - } - if (version < 3) { - dds_log(debug, "Applying pkg_db migration 3"); - migrate_repodb_3(db); - } - meta["version"] = current_database_version; - exec(db.prepare("UPDATE dds_cat_meta SET meta=?"), meta.dump()); - tr.commit(); - - if (version < 3 && !getenv_bool("DDS_NO_ADD_INITIAL_REPO")) { - // Version 3 introduced remote repositories. If we're updating to 3, add that repo now - dds_log(info, "Downloading initial repository"); - dds::add_init_repo(db); - } -} - -} // namespace - -fs::path pkg_db::default_path() noexcept { return dds_data_dir() / "pkgs.db"; } - -pkg_db pkg_db::open(const std::string& db_path) { - if (db_path != ":memory:") { - auto pardir = fs::weakly_canonical(db_path).parent_path(); - fs::create_directories(pardir); - } - dds_log(debug, "Opening package database [{}]", db_path); - auto db = nsql::database::open(db_path); - try { - ensure_migrated(db); - } catch (const nsql::sqlite3_error& e) { - dds_log(critical, - "Failed to load the package database. It appears to be invalid/corrupted. The " - "exception message is: {}", - e.what()); - throw_external_error(); - } - dds_log(trace, "Successfully opened database"); - return pkg_db(std::move(db)); -} - -pkg_db::pkg_db(nsql::database db) - : _db(std::move(db)) {} - -void pkg_db::store(const pkg_listing& pkg) { - nsql::transaction_guard tr{_db}; - do_store_pkg(_db, _stmt_cache, pkg); -} - -result pkg_db::get(const pkg_id& pk_id) const noexcept { - auto ver_str = pk_id.version.to_string(); - dds_log(trace, "Lookup package {}@{}", pk_id.name, ver_str); - auto& st = _stmt_cache(R"( - SELECT - pkg_id, - name, - version, - remote_url, - description - FROM dds_pkgs - WHERE name = ?1 AND version = ?2 - ORDER BY pkg_id DESC - )"_sql); - st.reset(); - st.bindings() = std::forward_as_tuple(pk_id.name, ver_str); - auto ec = st.step(std::nothrow); - if (ec == nsql::errc::done) { - return new_error([&] { - auto all_ids = this->all(); - auto id_strings - = ranges::views::transform(all_ids, [&](auto id) { return id.to_string(); }); - return e_nonesuch{pk_id.to_string(), did_you_mean(pk_id.to_string(), id_strings)}; - }); - } - neo_assert_always(invariant, - ec == nsql::errc::row, - "Failed to pull a package from the database", - ec, - pk_id.to_string(), - nsql::error_category().message(int(ec))); - - const auto& [pkg_id, name, version, remote_url, description] - = st.row().unpack(); - - ec = st.step(std::nothrow); - if (ec == nsql::errc::row) { - dds_log(warn, - "There is more than one entry for package {} in the database. One will be " - "chosen arbitrarily.", - pk_id.to_string()); - } - - neo_assert(invariant, - pk_id.name == name && pk_id.version == semver::version::parse(version), - "Package metadata does not match", - pk_id.to_string(), - name, - version); - - auto deps = dependencies_of(pk_id); - - auto info = pkg_listing{ - .ident = pk_id, - .deps = std::move(deps), - .description = std::move(description), - .remote_pkg = any_remote_pkg::from_url(neo::url::parse(remote_url)), - }; - - return info; -} - -auto pair_to_pkg_id = [](auto&& pair) { - const auto& [name, ver] = pair; - return pkg_id{name, semver::version::parse(ver)}; -}; - -std::vector pkg_db::all() const noexcept { - return nsql::exec_tuples( - _stmt_cache("SELECT name, version FROM dds_pkgs"_sql)) - | neo::lref // - | ranges::views::transform(pair_to_pkg_id) // - | ranges::to_vector; -} - -std::vector pkg_db::by_name(std::string_view sv) const noexcept { - return nsql::exec_tuples( // - _stmt_cache( - R"( - SELECT name, version - FROM dds_pkgs - WHERE name = ? - ORDER BY pkg_id DESC - )"_sql), - sv) // - | neo::lref // - | ranges::views::transform(pair_to_pkg_id) // - | ranges::to_vector; -} - -std::vector pkg_db::dependencies_of(const pkg_id& pkg) const noexcept { - dds_log(trace, "Lookup dependencies of {}@{}", pkg.name, pkg.version.to_string()); - return nsql::exec_tuples( // - _stmt_cache( - R"( - WITH this_pkg_id AS ( - SELECT pkg_id - FROM dds_pkgs - WHERE name = ? AND version = ? - ) - SELECT dep_name, low, high - FROM dds_pkg_deps - WHERE pkg_id IN this_pkg_id - ORDER BY dep_name - )"_sql), - pkg.name, - pkg.version.to_string()) // - | neo::lref // - | ranges::views::transform([](auto&& pair) { - auto& [name, low, high] = pair; - auto dep - = dependency{name, {semver::version::parse(low), semver::version::parse(high)}}; - dds_log(trace, " Depends: {}", dep.to_string()); - return dep; - }) // - | ranges::to_vector; -} diff --git a/src/dds/pkg/db.hpp b/src/dds/pkg/db.hpp deleted file mode 100644 index 3519a3ef..00000000 --- a/src/dds/pkg/db.hpp +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include "./listing.hpp" - -#include -#include - -#include -#include -#include - -#include -#include - -namespace dds { - -struct dependency; -struct pkg_id; - -class pkg_db { - neo::sqlite3::database _db; - mutable neo::sqlite3::statement_cache _stmt_cache{_db}; - - explicit pkg_db(neo::sqlite3::database db); - pkg_db(const pkg_db&) = delete; - -public: - pkg_db(pkg_db&&) = default; - pkg_db& operator=(pkg_db&&) = default; - - static pkg_db open(const std::string& db_path); - static pkg_db open(path_ref db_path) { return open(db_path.string()); } - - static fs::path default_path() noexcept; - - void store(const pkg_listing& info); - result get(const pkg_id& id) const noexcept; - - std::vector all() const noexcept; - std::vector by_name(std::string_view sv) const noexcept; - std::vector dependencies_of(const pkg_id& pkg) const noexcept; - - auto& database() noexcept { return _db; } - auto& database() const noexcept { return _db; } -}; - -} // namespace dds diff --git a/src/dds/pkg/db.test.cpp b/src/dds/pkg/db.test.cpp deleted file mode 100644 index c1dcf271..00000000 --- a/src/dds/pkg/db.test.cpp +++ /dev/null @@ -1,75 +0,0 @@ -#include - -#include - -using namespace std::literals; - -TEST_CASE("Create a simple database") { - // Just create and run migrations on an in-memory database - auto repo = dds::pkg_db::open(":memory:"s); -} - -TEST_CASE("Open a database in a non-ascii path") { - ::setlocale(LC_ALL, ".utf8"); - auto THIS_DIR = dds::fs::canonical(__FILE__).parent_path(); - auto BUILD_DIR - = (THIS_DIR.parent_path().parent_path().parent_path() / "_build").lexically_normal(); - auto subdir = BUILD_DIR / "Ю́рий Алексе́евич Гага́рин"; - dds::fs::remove_all(subdir); - dds::pkg_db::open(subdir / "test.db"); - dds::fs::remove_all(subdir); -} - -struct pkg_db_test_case { - dds::pkg_db db = dds::pkg_db::open(":memory:"s); -}; - -TEST_CASE_METHOD(pkg_db_test_case, "Store a simple package") { - db.store(dds::pkg_listing{ - dds::pkg_id{"foo", semver::version::parse("1.2.3")}, - {}, - "example", - dds::any_remote_pkg::from_url(neo::url::parse("git+http://example.com#master")), - }); - - auto pkgs = db.by_name("foo"); - REQUIRE(pkgs.size() == 1); - CHECK(pkgs[0].name == "foo"); - CHECK(pkgs[0].version == semver::version::parse("1.2.3")); - auto info = db.get(pkgs[0]); - REQUIRE(info); - CHECK(info->ident == pkgs[0]); - CHECK(info->deps.empty()); - CHECK(info->remote_pkg.to_url_string() == "git+http://example.com#master"); - - // Update the entry with a new git remote ref - CHECK_NOTHROW(db.store(dds::pkg_listing{ - dds::pkg_id{"foo", semver::version::parse("1.2.3")}, - {}, - "example", - dds::any_remote_pkg::from_url(neo::url::parse("git+http://example.com#develop")), - })); - // The previous pkg_id is still a valid lookup key - info = db.get(pkgs[0]); - REQUIRE(info); - CHECK(info->remote_pkg.to_url_string() == "git+http://example.com#develop"); -} - -TEST_CASE_METHOD(pkg_db_test_case, "Package requirements") { - db.store(dds::pkg_listing{ - dds::pkg_id{"foo", semver::version::parse("1.2.3")}, - { - {"bar", {semver::version::parse("1.2.3"), semver::version::parse("1.4.0")}}, - {"baz", {semver::version::parse("5.3.0"), semver::version::parse("6.0.0")}}, - }, - "example", - dds::any_remote_pkg::from_url(neo::url::parse("git+http://example.com#master")), - }); - auto pkgs = db.by_name("foo"); - REQUIRE(pkgs.size() == 1); - CHECK(pkgs[0].name == "foo"); - auto deps = db.dependencies_of(pkgs[0]); - CHECK(deps.size() == 2); - CHECK(deps[0].name == "bar"); - CHECK(deps[1].name == "baz"); -} diff --git a/src/dds/pkg/get/base.cpp b/src/dds/pkg/get/base.cpp deleted file mode 100644 index cd0a75fb..00000000 --- a/src/dds/pkg/get/base.cpp +++ /dev/null @@ -1,33 +0,0 @@ -#include "./base.hpp" - -#include -#include - -#include - -using namespace dds; - -// void remote_pkg_base::generate_auto_lib_files(const pkg_id& pid, path_ref root) const { -// if (auto_lib.has_value()) { -// dds_log(info, "Generating library data automatically"); - -// auto pkg_strm = open(root / "package.json5", std::ios::binary | std::ios::out); -// auto man_json = nlohmann::json::object(); -// man_json["name"] = pid.name; -// man_json["version"] = pid.version.to_string(); -// man_json["namespace"] = auto_lib->namespace_; -// pkg_strm << nlohmann::to_string(man_json); - -// auto lib_strm = open(root / "library.json5", std::ios::binary | std::ios::out); -// auto lib_json = nlohmann::json::object(); -// lib_json["name"] = auto_lib->name; -// lib_strm << nlohmann::to_string(lib_json); -// } -// } - -void remote_pkg_base::get_sdist(path_ref dest) const { get_raw_directory(dest); } -void remote_pkg_base::get_raw_directory(path_ref dest) const { do_get_raw(dest); } - -neo::url remote_pkg_base::to_url() const { return do_to_url(); } - -std::string remote_pkg_base::to_url_string() const { return to_url().to_string(); } \ No newline at end of file diff --git a/src/dds/pkg/get/base.hpp b/src/dds/pkg/get/base.hpp deleted file mode 100644 index e192f341..00000000 --- a/src/dds/pkg/get/base.hpp +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include -#include -#include - -#include -#include - -namespace dds { - -struct pkg_id; - -class remote_pkg_base { - virtual void do_get_raw(path_ref dest) const = 0; - virtual neo::url do_to_url() const = 0; - -public: - void get_sdist(path_ref dest) const; - void get_raw_directory(path_ref dest) const; - - neo::url to_url() const; - std::string to_url_string() const; -}; - -} // namespace dds diff --git a/src/dds/pkg/get/dds_http.cpp b/src/dds/pkg/get/dds_http.cpp deleted file mode 100644 index ea0963c2..00000000 --- a/src/dds/pkg/get/dds_http.cpp +++ /dev/null @@ -1,41 +0,0 @@ -#include "./dds_http.hpp" - -#include "./http.hpp" - -#include - -using namespace dds; - -neo::url dds_http_remote_pkg::do_to_url() const { - auto ret = repo_url; - ret.scheme = "dds+" + ret.scheme; - ret.path = fmt::format("{}/{}", ret.path, pkg_id.to_string()); - return ret; -} - -dds_http_remote_pkg dds_http_remote_pkg::from_url(const neo::url& url) { - auto repo_url = url; - if (repo_url.scheme.starts_with("dds+")) { - repo_url.scheme = repo_url.scheme.substr(4); - } else if (repo_url.scheme.ends_with("+dds")) { - repo_url.scheme = repo_url.scheme.substr(0, repo_url.scheme.size() - 4); - } else { - // Nothing to trim - } - - fs::path full_path = repo_url.path; - repo_url.path = full_path.parent_path().generic_string(); - auto pkg_id = dds::pkg_id::parse(full_path.filename().string()); - - return {repo_url, pkg_id}; -} - -void dds_http_remote_pkg::do_get_raw(path_ref dest) const { - auto http_url = repo_url; - fs::path path = fs::path(repo_url.path) / "pkg" / pkg_id.name / pkg_id.version.to_string() - / "sdist.tar.gz"; - http_url.path = path.lexically_normal().generic_string(); - http_remote_pkg http; - http.url = http_url; - http.get_raw_directory(dest); -} diff --git a/src/dds/pkg/get/dds_http.hpp b/src/dds/pkg/get/dds_http.hpp deleted file mode 100644 index 1ed7f238..00000000 --- a/src/dds/pkg/get/dds_http.hpp +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include "./base.hpp" - -#include - -#include - -#include -#include - -namespace dds { - -class dds_http_remote_pkg : public remote_pkg_base { - void do_get_raw(path_ref) const override; - neo::url do_to_url() const override; - -public: - neo::url repo_url; - dds::pkg_id pkg_id; - - dds_http_remote_pkg() = default; - - dds_http_remote_pkg(neo::url u, dds::pkg_id pid) - : repo_url(u) - , pkg_id(pid) {} - - static dds_http_remote_pkg from_url(const neo::url& url); -}; - -} // namespace dds diff --git a/src/dds/pkg/get/dds_http.test.cpp b/src/dds/pkg/get/dds_http.test.cpp deleted file mode 100644 index 5e5786c1..00000000 --- a/src/dds/pkg/get/dds_http.test.cpp +++ /dev/null @@ -1,12 +0,0 @@ -#include "./dds_http.hpp" - -#include - -TEST_CASE("Parse a URL") { - auto pkg = dds::dds_http_remote_pkg::from_url( - neo::url::parse("dds+http://foo.bar/repo-dir/egg@1.2.3")); - CHECK(pkg.repo_url.to_string() == "http://foo.bar/repo-dir"); - CHECK(pkg.pkg_id.name == "egg"); - CHECK(pkg.pkg_id.version.to_string() == "1.2.3"); - CHECK(pkg.to_url_string() == "dds+http://foo.bar/repo-dir/egg@1.2.3"); -} diff --git a/src/dds/pkg/get/get.cpp b/src/dds/pkg/get/get.cpp deleted file mode 100644 index 577dcd0c..00000000 --- a/src/dds/pkg/get/get.cpp +++ /dev/null @@ -1,67 +0,0 @@ -#include "./get.hpp" - -#include -#include -#include -#include -#include - -#include -#include -#include - -using namespace dds; - -temporary_sdist dds::get_package_sdist(const any_remote_pkg& rpkg) { - auto tmpdir = dds::temporary_dir::create(); - - rpkg.get_sdist(tmpdir.path()); - - auto sd_tmp_dir = dds::temporary_dir::create(); - sdist_params params{ - .project_dir = tmpdir.path(), - .dest_path = sd_tmp_dir.path(), - .force = true, - }; - auto sd = create_sdist(params); - return {sd_tmp_dir, sd}; -} - -temporary_sdist dds::get_package_sdist(const pkg_listing& pkg) { - auto tsd = get_package_sdist(pkg.remote_pkg); - if (!(tsd.sdist.manifest.id == pkg.ident)) { - throw_external_error( - "The package name@version in the generated source distribution does not match the name " - "listed in the remote listing file (expected '{}', but got '{}')", - pkg.ident.to_string(), - tsd.sdist.manifest.id.to_string()); - } - return tsd; -} - -void dds::get_all(const std::vector& pkgs, pkg_cache& repo, const pkg_db& cat) { - std::mutex repo_mut; - - auto absent_pkg_infos - = pkgs // - | ranges::views::filter([&](auto pk) { - std::scoped_lock lk{repo_mut}; - return !repo.find(pk); - }) - | ranges::views::transform([&](auto id) { - auto info = cat.get(id); - neo_assert(invariant, !!info, "No database entry for package id?", id.to_string()); - return *info; - }); - - auto okay = parallel_run(absent_pkg_infos, 8, [&](pkg_listing inf) { - dds_log(info, "Download package: {}", inf.ident.to_string()); - auto tsd = get_package_sdist(inf); - std::scoped_lock lk{repo_mut}; - repo.import_sdist(tsd.sdist, if_exists::throw_exc); - }); - - if (!okay) { - throw_external_error("Downloading of packages failed."); - } -} diff --git a/src/dds/pkg/get/get.hpp b/src/dds/pkg/get/get.hpp deleted file mode 100644 index b1ed0521..00000000 --- a/src/dds/pkg/get/get.hpp +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include -#include - -namespace dds { - -class pkg_cache; -class pkg_db; -struct pkg_listing; -class any_remote_pkg; - -temporary_sdist get_package_sdist(const any_remote_pkg&); -temporary_sdist get_package_sdist(const pkg_listing&); - -void get_all(const std::vector& pkgs, dds::pkg_cache& repo, const pkg_db& cat); - -} // namespace dds diff --git a/src/dds/pkg/get/git.cpp b/src/dds/pkg/get/git.cpp deleted file mode 100644 index 445fce49..00000000 --- a/src/dds/pkg/get/git.cpp +++ /dev/null @@ -1,59 +0,0 @@ -#include "./git.hpp" - -#include -#include -#include -#include - -#include -#include - -using namespace dds; -using namespace std::literals; - -git_remote_pkg git_remote_pkg::from_url(const neo::url& url) { - if (!url.fragment) { - BOOST_LEAF_THROW_EXCEPTION( - user_error( - "Git URL requires a fragment specified the Git ref to clone"), - DDS_E_ARG(e_url_string{url.to_string()})); - } - git_remote_pkg ret; - ret.url = url; - if (url.scheme.starts_with("git+")) { - ret.url.scheme = url.scheme.substr(4); - } else if (url.scheme.ends_with("+git")) { - ret.url.scheme = url.scheme.substr(0, url.scheme.size() - 4); - } else { - // Leave the URL as-is - } - ret.ref = *url.fragment; - ret.url.fragment.reset(); - return ret; -} - -neo::url git_remote_pkg::do_to_url() const { - neo::url ret = url; - ret.fragment = ref; - if (ret.scheme != "git") { - ret.scheme = "git+" + ret.scheme; - } - return ret; -} - -void git_remote_pkg::do_get_raw(path_ref dest) const { - fs::remove(dest); - dds_log(info, "Clone Git repository [{}] (at {}) to [{}]", url.to_string(), ref, dest.string()); - auto command - = {"git"s, "clone"s, "--depth=1"s, "--branch"s, ref, url.to_string(), dest.string()}; - auto git_res = run_proc(command); - if (!git_res.okay()) { - BOOST_LEAF_THROW_EXCEPTION( - make_external_error( - "Git clone operation failed [Git command: {}] [Exitted {}]:\n{}", - quote_command(command), - git_res.retc, - git_res.output), - url); - } -} diff --git a/src/dds/pkg/get/git.hpp b/src/dds/pkg/get/git.hpp deleted file mode 100644 index 01f87d4c..00000000 --- a/src/dds/pkg/get/git.hpp +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include "./base.hpp" - -#include - -#include - -namespace dds { - -class git_remote_pkg : public remote_pkg_base { - void do_get_raw(path_ref) const override; - neo::url do_to_url() const override; - -public: - neo::url url; - std::string ref; - - static git_remote_pkg from_url(const neo::url&); -}; - -} // namespace dds diff --git a/src/dds/pkg/get/git.test.cpp b/src/dds/pkg/get/git.test.cpp deleted file mode 100644 index aa32d1ee..00000000 --- a/src/dds/pkg/get/git.test.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include "./git.hpp" - -#include - -TEST_CASE("Round-trip a URL") { - auto git = dds::git_remote_pkg::from_url( - neo::url::parse("http://github.com/vector-of-bool/neo-fun.git#0.4.0")); - CHECK(git.to_url_string() == "git+http://github.com/vector-of-bool/neo-fun.git#0.4.0"); -} diff --git a/src/dds/pkg/get/github.cpp b/src/dds/pkg/get/github.cpp deleted file mode 100644 index 12e824c1..00000000 --- a/src/dds/pkg/get/github.cpp +++ /dev/null @@ -1,42 +0,0 @@ -#include "./github.hpp" - -#include "./http.hpp" - -#include -#include - -#include -#include - -using namespace dds; - -neo::url github_remote_pkg::do_to_url() const { - neo::url ret; - ret.scheme = "github"; - ret.path = fmt::format("{}/{}/{}", owner, reponame, ref); - return ret; -} - -void github_remote_pkg::do_get_raw(path_ref dest) const { - http_remote_pkg http; - auto new_url = fmt::format("https://github.com/{}/{}/archive/{}.tar.gz", owner, reponame, ref); - http.url = neo::url::parse(new_url); - http.strip_n_components = 1; - http.get_raw_directory(dest); -} - -github_remote_pkg github_remote_pkg::from_url(const neo::url& url) { - fs::path path = url.path; - if (ranges::distance(path) != 3) { - BOOST_LEAF_THROW_EXCEPTION(make_user_error( - "'github:' URLs should have a path with three segments"), - url); - } - github_remote_pkg ret; - // Split the three path elements as {owner}/{reponame}/{git-ref} - auto elem_iter = path.begin(); - ret.owner = (*elem_iter++).generic_string(); - ret.reponame = (*elem_iter++).generic_string(); - ret.ref = (*elem_iter).generic_string(); - return ret; -} diff --git a/src/dds/pkg/get/github.hpp b/src/dds/pkg/get/github.hpp deleted file mode 100644 index c052201e..00000000 --- a/src/dds/pkg/get/github.hpp +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include "./base.hpp" - -#include - -#include -#include - -namespace dds { - -class github_remote_pkg : public remote_pkg_base { - void do_get_raw(path_ref) const override; - neo::url do_to_url() const override; - -public: - std::string owner; - std::string reponame; - std::string ref; - - static github_remote_pkg from_url(const neo::url&); -}; - -} // namespace dds diff --git a/src/dds/pkg/get/github.test.cpp b/src/dds/pkg/get/github.test.cpp deleted file mode 100644 index 57f33bd3..00000000 --- a/src/dds/pkg/get/github.test.cpp +++ /dev/null @@ -1,11 +0,0 @@ -#include "./github.hpp" - -#include - -TEST_CASE("Parse a github: URL") { - auto gh_pkg - = dds::github_remote_pkg::from_url(neo::url::parse("github:vector-of-bool/neo-fun/0.6.0")); - CHECK(gh_pkg.owner == "vector-of-bool"); - CHECK(gh_pkg.reponame == "neo-fun"); - CHECK(gh_pkg.ref == "0.6.0"); -} diff --git a/src/dds/pkg/get/http.cpp b/src/dds/pkg/get/http.cpp deleted file mode 100644 index 02d2e165..00000000 --- a/src/dds/pkg/get/http.cpp +++ /dev/null @@ -1,123 +0,0 @@ -#include "./http.hpp" - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -using namespace dds; - -void http_remote_pkg::do_get_raw(path_ref dest) const { - dds_log(trace, "Downloading remote package via HTTP from [{}]", url.to_string()); - - if (url.scheme != "http" && url.scheme != "https") { - dds_log(error, "Unsupported URL scheme '{}' (in [{}])", url.scheme, url.to_string()); - BOOST_LEAF_THROW_EXCEPTION(user_error( - "The given URL download is not supported. (Only 'http' and " - "'https' URLs are supported)"), - DDS_E_ARG(e_url_string{url.to_string()})); - } - - neo_assert(invariant, - !!url.host, - "The given URL did not have a host part. This shouldn't be possible... Please file " - "a bug report.", - url.to_string()); - - // Create a temporary directory in which to download the archive - auto tdir = dds::temporary_dir::create(); - // For ease of debugging, use the filename from the URL, if possible - auto fname = fs::path(url.path).filename(); - if (fname.empty()) { - fname = "dds-download.tmp"; - } - auto dl_path = tdir.path() / fname; - fs::create_directories(tdir.path()); - - // Download the file! - { - auto& pool = http_pool::thread_local_pool(); - auto [client, resp] = pool.request(url); - auto dl_file = neo::file_stream::open(dl_path, neo::open_mode::write); - client.recv_body_into(resp, neo::stream_io_buffers{dl_file}); - } - - fs::create_directories(fs::absolute(dest)); - dds_log(debug, "Expanding downloaded package archive into [{}]", dest.string()); - std::ifstream infile{dl_path, std::ios::binary}; - try { - neo::expand_directory_targz( - neo::expand_options{ - .destination_directory = dest, - .input_name = dl_path.string(), - .strip_components = this->strip_n_components, - }, - infile); - } catch (const std::runtime_error& err) { - throw_external_error( - "The file downloaded from [{}] failed to extract (Inner error: {})", - url.to_string(), - err.what()); - } -} - -http_remote_pkg http_remote_pkg::from_url(const neo::url& url) { - neo_assert(expects, - url.scheme == neo::oper::any_of("http", "https"), - "Invalid URL for an HTTP remote", - url.to_string()); - - neo::url ret_url = url; - if (url.fragment) { - dds_log(warn, - "Fragment '{}' in URL [{}] will have no effect", - *url.fragment, - url.to_string()); - ret_url.fragment.reset(); - } - - ret_url.query = {}; - - unsigned n_strpcmp = 0; - - if (url.query) { - std::string query_acc; - - neo::basic_query_string_view qsv{*url.query}; - for (auto qstr : qsv) { - if (qstr.key_raw() == "__dds_strpcmp") { - n_strpcmp = static_cast(std::stoul(qstr.value_decoded())); - } else { - if (!query_acc.empty()) { - query_acc.push_back(';'); - } - query_acc.append(qstr.string()); - } - } - if (!query_acc.empty()) { - ret_url.query = query_acc; - } - } - - return {ret_url, n_strpcmp}; -} - -neo::url http_remote_pkg::do_to_url() const { - auto ret_url = url; - if (strip_n_components != 0) { - auto strpcmp_param = fmt::format("__dds_strpcmp={}", strip_n_components); - if (ret_url.query) { - *ret_url.query += ";" + strpcmp_param; - } else { - ret_url.query = strpcmp_param; - } - } - return ret_url; -} diff --git a/src/dds/pkg/get/http.hpp b/src/dds/pkg/get/http.hpp deleted file mode 100644 index 03d6ee10..00000000 --- a/src/dds/pkg/get/http.hpp +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include "./base.hpp" - -#include - -#include -#include - -namespace dds { - -class http_remote_pkg : public remote_pkg_base { - void do_get_raw(path_ref) const override; - neo::url do_to_url() const override; - -public: - neo::url url; - unsigned strip_n_components = 0; - - http_remote_pkg() = default; - - http_remote_pkg(neo::url u, unsigned strpcmp) - : url(u) - , strip_n_components(strpcmp) {} - - static http_remote_pkg from_url(const neo::url& url); -}; - -} // namespace dds diff --git a/src/dds/pkg/get/http.test.cpp b/src/dds/pkg/get/http.test.cpp deleted file mode 100644 index b71eabbe..00000000 --- a/src/dds/pkg/get/http.test.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include "./http.hpp" - -#include -#include -#include - -#include - -TEST_CASE("Convert from URL") { - auto listing = dds::http_remote_pkg::from_url(neo::url::parse("http://example.org/foo")); - CHECK(listing.to_url_string() == "http://example.org/foo"); - listing.strip_n_components = 4; - CHECK(listing.to_url_string() == "http://example.org/foo?__dds_strpcmp=4"); - - listing = dds::http_remote_pkg::from_url( - neo::url::parse("http://example.org/foo?bar=baz;__dds_strpcmp=7;thing=foo#fragment")); - CHECK(listing.strip_n_components == 7); - CHECK(listing.to_url_string() == "http://example.org/foo?bar=baz;thing=foo;__dds_strpcmp=7"); -} diff --git a/src/dds/pkg/id.cpp b/src/dds/pkg/id.cpp deleted file mode 100644 index a5995758..00000000 --- a/src/dds/pkg/id.cpp +++ /dev/null @@ -1,30 +0,0 @@ -#include - -#include -#include - -#include - -#include - -using namespace dds; - -pkg_id pkg_id::parse(const std::string_view s) { - DDS_E_SCOPE(e_invalid_pkg_id_str{std::string(s)}); - auto at_pos = s.find('@'); - if (at_pos == s.npos) { - BOOST_LEAF_THROW_EXCEPTION( - make_user_error("Package ID must contain an '@' symbol")); - } - - auto name = s.substr(0, at_pos); - auto ver_str = s.substr(at_pos + 1); - - try { - return {std::string(name), semver::version::parse(ver_str)}; - } catch (const semver::invalid_version& err) { - BOOST_LEAF_THROW_EXCEPTION(make_user_error(), err); - } -} - -std::string pkg_id::to_string() const noexcept { return name + "@" + version.to_string(); } diff --git a/src/dds/pkg/id.hpp b/src/dds/pkg/id.hpp deleted file mode 100644 index 82447542..00000000 --- a/src/dds/pkg/id.hpp +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include - -#include -#include -#include - -namespace dds { - -struct e_invalid_pkg_id_str { - std::string value; -}; - -/** - * Represents a unique package ID. We store this as a simple name-version pair. - * - * In text, this is represented with an `@` symbol in between. The `parse` and - * `to_string` method convert between this textual representation, and supports - * full round-trips. - */ -struct pkg_id { - /// The name of the package - std::string name; - /// The version of the package - semver::version version; - - /** - * Parse the given string into a pkg_id object. - */ - static pkg_id parse(std::string_view); - - /**d - * Convert this pkg_id into its corresponding textual representation. - * The returned string can be passed back to `parse()` for a round-trip - */ - std::string to_string() const noexcept; - - friend bool operator<(const pkg_id& lhs, const pkg_id& rhs) noexcept { - return std::tie(lhs.name, lhs.version) < std::tie(rhs.name, rhs.version); - } - friend bool operator==(const pkg_id& lhs, const pkg_id& rhs) noexcept { - return std::tie(lhs.name, lhs.version) == std::tie(rhs.name, rhs.version); - } -}; - -} // namespace dds diff --git a/src/dds/pkg/listing.cpp b/src/dds/pkg/listing.cpp deleted file mode 100644 index 0bd70256..00000000 --- a/src/dds/pkg/listing.cpp +++ /dev/null @@ -1,54 +0,0 @@ -#include "./listing.hpp" - -#include "./get/dds_http.hpp" -#include "./get/git.hpp" -#include "./get/github.hpp" -#include "./get/http.hpp" - -#include -#include -#include - -#include -#include -#include - -using namespace dds; - -any_remote_pkg::~any_remote_pkg() = default; -any_remote_pkg::any_remote_pkg() {} - -static std::shared_ptr do_parse_url(const neo::url& url) { - if (url.scheme == neo::oper::any_of("http", "https")) { - return std::make_shared(http_remote_pkg::from_url(url)); - } else if (url.scheme - == neo::oper::any_of("git", "git+https", "git+http", "https+git", "http+git")) { - return std::make_shared(git_remote_pkg::from_url(url)); - } else if (url.scheme == "github") { - return std::make_shared(github_remote_pkg::from_url(url)); - } else if (url.scheme == neo::oper::any_of("dds+http", "http+dds", "dds+https", "https+dds")) { - return std::make_shared(dds_http_remote_pkg::from_url(url)); - } else { - BOOST_LEAF_THROW_EXCEPTION(make_user_error( - "Unknown scheme '{}' for remote package listing URL", - url.scheme), - url); - } -} - -any_remote_pkg any_remote_pkg::from_url(const neo::url& url) { - auto ptr = do_parse_url(url); - return any_remote_pkg(ptr); -} - -neo::url any_remote_pkg::to_url() const { - neo_assert(expects, !!_impl, "Accessing an inactive any_remote_pkg"); - return _impl->to_url(); -} - -std::string any_remote_pkg::to_url_string() const { return to_url().to_string(); } - -void any_remote_pkg::get_sdist(path_ref dest) const { - neo_assert(expects, !!_impl, "Accessing an inactive any_remote_pkg"); - _impl->get_sdist(dest); -} diff --git a/src/dds/pkg/listing.hpp b/src/dds/pkg/listing.hpp deleted file mode 100644 index 1a6ec68d..00000000 --- a/src/dds/pkg/listing.hpp +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once - -#include -#include - -#include - -#include -#include -#include -#include - -namespace dds { - -class remote_pkg_base; - -class any_remote_pkg { - std::shared_ptr _impl; - - explicit any_remote_pkg(std::shared_ptr p) - : _impl(p) {} - -public: - any_remote_pkg(); - ~any_remote_pkg(); - - static any_remote_pkg from_url(const neo::url& url); - - neo::url to_url() const; - std::string to_url_string() const; - void get_sdist(path_ref dest) const; -}; - -struct pkg_listing { - pkg_id ident; - std::vector deps{}; - std::string description{}; - - any_remote_pkg remote_pkg{}; -}; - -} // namespace dds diff --git a/src/dds/pkg/listing.test.cpp b/src/dds/pkg/listing.test.cpp deleted file mode 100644 index 3bf0407d..00000000 --- a/src/dds/pkg/listing.test.cpp +++ /dev/null @@ -1,12 +0,0 @@ -#include "./listing.hpp" - -#include - -TEST_CASE("Round trip a URL") { - auto listing - = dds::any_remote_pkg::from_url(neo::url::parse("http://example.org/package.tar.gz")); - CHECK(listing.to_url_string() == "http://example.org/package.tar.gz"); - - listing = dds::any_remote_pkg::from_url(neo::url::parse("git://example.org/repo#wat")); - CHECK(listing.to_url_string() == "git://example.org/repo#wat"); -} diff --git a/src/dds/pkg/remote.cpp b/src/dds/pkg/remote.cpp deleted file mode 100644 index 82c385e9..00000000 --- a/src/dds/pkg/remote.cpp +++ /dev/null @@ -1,309 +0,0 @@ -#include "./remote.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace dds; -using namespace fansi::literals; -namespace nsql = neo::sqlite3; - -namespace { - -struct remote_db { - temporary_dir _tempdir; - nsql::database db; - - static remote_db download_and_open(http_client& client, const http_response_info& resp) { - auto tempdir = temporary_dir::create(); - auto repo_db_dl = tempdir.path() / "repo.db"; - fs::create_directories(tempdir.path()); - auto outfile = neo::file_stream::open(repo_db_dl, neo::open_mode::write); - client.recv_body_into(resp, neo::stream_io_buffers(outfile)); - - auto db = nsql::open(repo_db_dl.string()); - return {tempdir, std::move(db)}; - } -}; - -} // namespace - -pkg_remote pkg_remote::connect(std::string_view url_str) { - DDS_E_SCOPE(e_url_string{std::string(url_str)}); - const auto url = neo::url::parse(url_str); - - auto& pool = http_pool::global_pool(); - auto db_url = url; - while (db_url.path.ends_with("/")) - db_url.path.pop_back(); - auto full_path = fmt::format("{}/{}", db_url.path, "repo.db"); - db_url.path = full_path; - auto [client, resp] = pool.request(db_url, http_request_params{.method = "GET"}); - auto db = remote_db::download_and_open(client, resp); - - auto name_st = db.db.prepare("SELECT name FROM dds_repo_meta"); - auto [name] = nsql::unpack_single(name_st); - - return {name, url}; -} - -void pkg_remote::store(nsql::database_ref db) { - auto st = db.prepare(R"( - INSERT INTO dds_pkg_remotes (name, url) - VALUES (?, ?) - ON CONFLICT (name) DO - UPDATE SET url = ?2 - )"); - nsql::exec(st, _name, _base_url.to_string()); -} - -void pkg_remote::update_pkg_db(nsql::database_ref db, - std::optional etag, - std::optional db_mtime) { - dds_log(info, - "Pulling repository contents for .cyan[{}] [{}`]"_styled, - _name, - _base_url.to_string()); - - auto& pool = http_pool::global_pool(); - auto url = _base_url; - while (url.path.ends_with("/")) - url.path.pop_back(); - auto full_path = fmt::format("{}/{}", url.path, "repo.db"); - url.path = full_path; - auto [client, resp] = pool.request(url, - http_request_params{ - .method = "GET", - .prior_etag = etag.value_or(""), - .last_modified = db_mtime.value_or(""), - }); - if (resp.not_modified()) { - // Cache hit - dds_log(info, "Package database {} is up-to-date", _name); - client.discard_body(resp); - return; - } - - auto rdb = remote_db::download_and_open(client, resp); - - auto base_url_str = _base_url.to_string(); - while (base_url_str.ends_with("/")) { - base_url_str.pop_back(); - } - - auto db_path = rdb._tempdir.path() / "repo.db"; - - auto rid_st = db.prepare("SELECT remote_id FROM dds_pkg_remotes WHERE name = ?"); - rid_st.bindings()[1] = _name; - auto [remote_id] = nsql::unpack_single(rid_st); - rid_st.reset(); - - dds_log(trace, "Attaching downloaded database"); - nsql::exec(db.prepare("ATTACH DATABASE ? AS remote"), db_path.string()); - neo_defer { db.exec("DETACH DATABASE remote"); }; - nsql::transaction_guard tr{db}; - dds_log(trace, "Clearing prior contents"); - nsql::exec( // - db.prepare(R"( - DELETE FROM dds_pkgs - WHERE remote_id = ? - )"), - remote_id); - dds_log(trace, "Importing packages"); - nsql::exec( // - db.prepare(R"( - INSERT INTO dds_pkgs - (name, version, description, remote_url, remote_id) - SELECT - name, - version, - description, - CASE - WHEN url LIKE 'dds:%@%' THEN - -- Convert 'dds:name@ver' to 'dds+/name@ver' - -- This will later resolve to the actual package URL - printf('dds+%s/%s', ?2, substr(url, 5)) - ELSE - -- Non-'dds:' URLs are kept as-is - url - END, - ?1 - FROM remote.dds_repo_packages - )"), - remote_id, - base_url_str); - dds_log(trace, "Importing dependencies"); - db.exec(R"( - INSERT OR REPLACE INTO dds_pkg_deps (pkg_id, dep_name, low, high) - SELECT - local_pkgs.pkg_id AS pkg_id, - dep_name, - low, - high - FROM remote.dds_repo_package_deps AS deps, - remote.dds_repo_packages AS pkgs USING(package_id), - dds_pkgs AS local_pkgs USING(name, version) - )"); - // Validate our database - dds_log(trace, "Running integrity check"); - auto fk_check = db.prepare("PRAGMA foreign_key_check"); - auto rows = nsql::iter_tuples(fk_check); - bool any_failed = false; - for (auto [child_table, rowid, parent_table, failed_idx] : rows) { - dds_log( - critical, - "Database foreign_key error after import: {0}.{3} referencing {2} violated at row {1}", - child_table, - rowid, - parent_table, - failed_idx); - any_failed = true; - } - auto int_check = db.prepare("PRAGMA main.integrity_check"); - for (auto [error] : nsql::iter_tuples(int_check)) { - if (error == "ok") { - continue; - } - dds_log(critical, "Database errors after import: {}", error); - any_failed = true; - } - if (any_failed) { - throw_external_error( - "Database update failed due to data integrity errors"); - } - - // Save the cache info for the remote - if (auto new_etag = resp.etag()) { - nsql::exec(db.prepare("UPDATE dds_pkg_remotes SET db_etag = ? WHERE name = ?"), - *new_etag, - _name); - } - if (auto mtime = resp.last_modified()) { - nsql::exec(db.prepare("UPDATE dds_pkg_remotes SET db_mtime = ? WHERE name = ?"), - *mtime, - _name); - } -} - -void dds::update_all_remotes(nsql::database_ref db) { - dds_log(info, "Updating catalog from all remotes"); - auto repos_st = db.prepare("SELECT name, url, db_etag, db_mtime FROM dds_pkg_remotes"); - auto tups = nsql::iter_tuples, - std::optional>(repos_st) - | ranges::to_vector; - - for (const auto& [name, url, etag, db_mtime] : tups) { - DDS_E_SCOPE(e_url_string{url}); - pkg_remote repo{name, neo::url::parse(url)}; - repo.update_pkg_db(db, etag, db_mtime); - } - - dds_log(info, "Recompacting database..."); - db.exec("VACUUM"); -} - -void dds::remove_remote(pkg_db& pkdb, std::string_view name) { - auto& db = pkdb.database(); - nsql::transaction_guard tr{db}; - auto get_rowid_st = db.prepare("SELECT remote_id FROM dds_pkg_remotes WHERE name = ?"); - get_rowid_st.bindings()[1] = name; - auto row = nsql::unpack_single_opt(get_rowid_st); - if (!row) { - BOOST_LEAF_THROW_EXCEPTION( // - make_user_error("There is no remote with name '{}'", - name), - [&] { - auto all_st = db.prepare("SELECT name FROM dds_pkg_remotes"); - auto tups = nsql::iter_tuples(all_st); - auto names = tups | ranges::views::transform([](auto&& tup) { - auto&& [n] = tup; - return n; - }) - | ranges::to_vector; - return e_nonesuch{name, did_you_mean(name, names)}; - }); - } - auto [rowid] = *row; - nsql::exec(db.prepare("DELETE FROM dds_pkg_remotes WHERE remote_id = ?"), rowid); -} - -void dds::add_init_repo(nsql::database_ref db) noexcept { - std::string_view init_repo = "https://repo-1.dds.pizza"; - // _Do not_ let errors stop us from continuing - bool okay = boost::leaf::try_catch( - [&]() -> bool { - try { - auto remote = pkg_remote::connect(init_repo); - remote.store(db); - update_all_remotes(db); - return true; - } catch (...) { - capture_exception(); - } - }, - [](http_status_error err, http_response_info resp, neo::url url) { - dds_log(error, - "An HTTP error occurred while adding the initial repository [{}]: HTTP Status " - "{} {}", - err.what(), - url.to_string(), - resp.status, - resp.status_message); - return false; - }, - [](e_sqlite3_error_exc e, neo::url url) { - dds_log(error, - "Error accessing remote database while adding initial repository: {}: {}", - url.to_string(), - e.message); - return false; - }, - [](e_sqlite3_error_exc e) { - dds_log(error, "Unexpected database error: {}", e.message); - return false; - }, - [](e_system_error_exc e, network_origin conn) { - dds_log(error, - "Error communicating with [.br.red[{}://{}:{}]`]: {}"_styled, - conn.protocol, - conn.hostname, - conn.port, - e.message); - return false; - }, - [](boost::leaf::diagnostic_info const& diag) -> int { - dds_log(critical, "Unhandled error while adding initial package repository: ", diag); - throw; - }); - if (!okay) { - dds_log(warn, "We failed to add the initial package repository [{}]", init_repo); - dds_log(warn, "No remote packages will be available until the above issue is resolved."); - dds_log( - warn, - "The remote package repository can be added again with [.br.yellow[dds pkg repo add \"{}\"]]"_styled, - init_repo); - } -} diff --git a/src/dds/pkg/remote.hpp b/src/dds/pkg/remote.hpp deleted file mode 100644 index 4310da3a..00000000 --- a/src/dds/pkg/remote.hpp +++ /dev/null @@ -1,39 +0,0 @@ -#pragma once - -#include -#include - -#include - -namespace dds { - -class pkg_db; - -struct e_remote_name { - std::string value; -}; - -class pkg_remote { - std::string _name; - neo::url _base_url; - -public: - pkg_remote(std::string name, neo::url url) - : _name(std::move(name)) - , _base_url(std::move(url)) {} - pkg_remote() = default; - - static pkg_remote connect(std::string_view url); - - void store(neo::sqlite3::database_ref); - void update_pkg_db(neo::sqlite3::database_ref, - std::optional etag = {}, - std::optional last_modified = {}); -}; - -void update_all_remotes(neo::sqlite3::database_ref); -void remove_remote(pkg_db& db, std::string_view name); - -void add_init_repo(neo::sqlite3::database_ref db) noexcept; - -} // namespace dds diff --git a/src/dds/pkg/search.cpp b/src/dds/pkg/search.cpp deleted file mode 100644 index 0d3cd786..00000000 --- a/src/dds/pkg/search.cpp +++ /dev/null @@ -1,76 +0,0 @@ -#include "./search.hpp" - -#include -#include -#include -#include -#include - -#include -#include - -#include -#include -#include - -using namespace dds; -namespace nsql = neo::sqlite3; - -result dds::pkg_search(nsql::database_ref db, - std::optional pattern) noexcept { - auto search_st = db.prepare(R"( - SELECT pkg.name, - group_concat(version, ';;'), - description, - remote.name, - remote.url - FROM dds_pkgs AS pkg - JOIN dds_pkg_remotes AS remote USING(remote_id) - WHERE lower(pkg.name) GLOB lower(:pattern) - GROUP BY pkg.name, remote_id, description - ORDER BY remote.name, pkg.name - )"); - // If no pattern, grab _everything_ - auto final_pattern = pattern.value_or("*"); - dds_log(debug, "Searching for packages matching pattern '{}'", final_pattern); - search_st.bindings()[1] = final_pattern; - auto rows = nsql::iter_tuples( - search_st); - - std::vector found; - for (auto [name, versions, desc, remote_name, remote_url] : rows) { - dds_log(debug, - "Found: {} with versions {} (Description: {}) from {} [{}]", - name, - versions, - desc, - remote_name, - remote_url); - auto version_strs = split(versions, ";;"); - auto versions_semver - = version_strs | ranges::views::transform(&semver::version::parse) | ranges::to_vector; - ranges::sort(versions_semver); - found.push_back(pkg_group_search_result{ - .name = name, - .versions = versions_semver, - .description = desc, - .remote_name = remote_name, - }); - } - - if (found.empty()) { - return boost::leaf::new_error([&] { - auto names_st = db.prepare("SELECT DISTINCT name from dds_pkgs"); - auto tups = nsql::iter_tuples(names_st); - auto names_vec = tups | ranges::views::transform([](auto&& row) { - auto [name] = row; - return name; - }) - | ranges::to_vector; - auto nearest = dds::did_you_mean(final_pattern, names_vec); - return e_nonesuch{final_pattern, nearest}; - }); - } - - return pkg_search_results{.found = std::move(found)}; -} diff --git a/src/dds/pkg/search.hpp b/src/dds/pkg/search.hpp deleted file mode 100644 index 633b61be..00000000 --- a/src/dds/pkg/search.hpp +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include - -#include - -#include -#include -#include - -namespace neo::sqlite3 { - -class database_ref; - -} // namespace neo::sqlite3 - -namespace dds { - -struct pkg_group_search_result { - std::string name; - std::vector versions; - std::string description; - std::string remote_name; -}; - -struct pkg_search_results { - std::vector found; -}; - -result pkg_search(neo::sqlite3::database_ref db, - std::optional query) noexcept; - -} // namespace dds diff --git a/src/dds/repoman/repoman.cpp b/src/dds/repoman/repoman.cpp deleted file mode 100644 index 1afd326c..00000000 --- a/src/dds/repoman/repoman.cpp +++ /dev/null @@ -1,240 +0,0 @@ -#include "./repoman.hpp" - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -using namespace dds; - -namespace nsql = neo::sqlite3; -using namespace nsql::literals; - -namespace { - -void migrate_db_1(nsql::database_ref db) { - db.exec(R"( - CREATE TABLE dds_repo_packages ( - package_id INTEGER PRIMARY KEY, - name TEXT NOT NULL, - version TEXT NOT NULL, - description TEXT NOT NULL, - url TEXT NOT NULL, - UNIQUE (name, version) - ); - - CREATE TABLE dds_repo_package_deps ( - dep_id INTEGER PRIMARY KEY, - package_id INTEGER NOT NULL - REFERENCES dds_repo_packages - ON DELETE CASCADE, - dep_name TEXT NOT NULL, - low TEXT NOT NULL, - high TEXT NOT NULL, - UNIQUE(package_id, dep_name) - ); - )"); -} - -void ensure_migrated(nsql::database_ref db, std::optional name) { - db.exec(R"( - PRAGMA busy_timeout = 6000; - PRAGMA foreign_keys = 1; - CREATE TABLE IF NOT EXISTS dds_repo_meta ( - meta_version INTEGER DEFAULT 1, - version INTEGER NOT NULL, - name TEXT NOT NULL - ); - - -- Insert the initial metadata - INSERT INTO dds_repo_meta (version, name) - SELECT 0, 'dds-repo-' || lower(hex(randomblob(6))) - WHERE NOT EXISTS (SELECT 1 FROM dds_repo_meta); - )"); - nsql::transaction_guard tr{db}; - - auto meta_st = db.prepare("SELECT version FROM dds_repo_meta"); - auto [version] = nsql::unpack_single(meta_st); - - constexpr int current_database_version = 1; - if (version < 1) { - migrate_db_1(db); - } - - nsql::exec(db.prepare("UPDATE dds_repo_meta SET version=?"), current_database_version); - if (name) { - nsql::exec(db.prepare("UPDATE dds_repo_meta SET name=?"), *name); - } -} - -} // namespace - -repo_manager repo_manager::create(path_ref directory, std::optional name) { - { - DDS_E_SCOPE(e_init_repo{directory}); - fs::create_directories(directory); - auto db_path = directory / "repo.db"; - auto db = nsql::database::open(db_path.string()); - DDS_E_SCOPE(e_init_repo_db{db_path}); - DDS_E_SCOPE(e_open_repo_db{db_path}); - ensure_migrated(db, name); - fs::create_directories(directory / "pkg"); - } - return open(directory); -} - -repo_manager repo_manager::open(path_ref directory) { - DDS_E_SCOPE(e_open_repo{directory}); - auto db_path = directory / "repo.db"; - DDS_E_SCOPE(e_open_repo_db{db_path}); - if (!fs::is_regular_file(db_path)) { - throw std::system_error(make_error_code(std::errc::no_such_file_or_directory), - "The database file does not exist"); - } - auto db = nsql::database::open(db_path.string()); - ensure_migrated(db, std::nullopt); - return repo_manager{fs::canonical(directory), std::move(db)}; -} - -std::string repo_manager::name() const noexcept { - auto [name] = nsql::unpack_single(_stmts("SELECT name FROM dds_repo_meta"_sql)); - return name; -} - -void repo_manager::import_targz(path_ref tgz_file) { - neo_assertion_breadcrumbs("Importing targz file", tgz_file.string()); - DDS_E_SCOPE(e_repo_import_targz{tgz_file}); - dds_log(info, "Importing sdist archive [{}]", tgz_file.string()); - neo::ustar_reader tar{ - neo::buffer_transform_source{neo::stream_io_buffers{ - neo::file_stream::open(tgz_file, neo::open_mode::read)}, - neo::gzip_decompressor{neo::inflate_decompressor{}}}}; - - std::optional man; - - for (auto mem : tar) { - if (fs::path(mem.filename_str()).lexically_normal() - == neo::oper::none_of("package.jsonc", "package.json5", "package.json")) { - continue; - } - - auto content = tar.all_data(); - auto synth_filename = tgz_file / mem.filename_str(); - man = package_manifest::load_from_json5_str(std::string_view(content), - synth_filename.string()); - break; - } - - if (!man) { - dds_log(critical, - "Given archive [{}] does not contain a package manifest file", - tgz_file.string()); - throw std::runtime_error("Invalid package archive"); - } - - DDS_E_SCOPE(man->id); - - neo::sqlite3::transaction_guard tr{_db}; - - dds_log(debug, "Recording package {}@{}", man->id.name, man->id.version.to_string()); - dds::pkg_listing info{.ident = man->id, - .deps = man->dependencies, - .description = "[No description]", - .remote_pkg = {}}; - auto rel_url = fmt::format("dds:{}", man->id.to_string()); - add_pkg(info, rel_url); - - auto dest_path = pkg_dir() / man->id.name / man->id.version.to_string() / "sdist.tar.gz"; - fs::create_directories(dest_path.parent_path()); - fs::copy(tgz_file, dest_path); - - tr.commit(); -} - -void repo_manager::delete_package(pkg_id pkg_id) { - neo::sqlite3::transaction_guard tr{_db}; - - DDS_E_SCOPE(pkg_id); - - nsql::exec( // - _stmts(R"( - DELETE FROM dds_repo_packages - WHERE name = ? - AND version = ? - )"_sql), - pkg_id.name, - pkg_id.version.to_string()); - /// XXX: Verify with _db.changes() that we actually deleted one row - - auto name_dir = pkg_dir() / pkg_id.name; - auto ver_dir = name_dir / pkg_id.version.to_string(); - - DDS_E_SCOPE(e_repo_delete_path{ver_dir}); - - if (!fs::is_directory(ver_dir)) { - throw std::system_error(std::make_error_code(std::errc::no_such_file_or_directory), - "No source archive for the requested package"); - } - - fs::remove_all(ver_dir); - - tr.commit(); - - std::error_code ec; - fs::remove(name_dir, ec); - if (ec && ec != std::errc::directory_not_empty) { - throw std::system_error(ec, "Failed to delete package name directory"); - } -} - -void repo_manager::add_pkg(const pkg_listing& info, std::string_view url) { - dds_log(info, "Directly add an entry for {}", info.ident.to_string()); - DDS_E_SCOPE(info.ident); - nsql::recursive_transaction_guard tr{_db}; - nsql::exec( // - _stmts(R"( - INSERT INTO dds_repo_packages (name, version, description, url) - VALUES (?, ?, ?, ?) - )"_sql), - info.ident.name, - info.ident.version.to_string(), - info.description, - url); - - auto package_rowid = _db.last_insert_rowid(); - - auto& insert_dep_st = _stmts(R"( - INSERT INTO dds_repo_package_deps(package_id, dep_name, low, high) - VALUES (?, ?, ?, ?) - )"_sql); - for (auto& dep : info.deps) { - assert(dep.versions.num_intervals() == 1); - auto iv_1 = *dep.versions.iter_intervals().begin(); - dds_log(trace, " Depends on: {}", dep.to_string()); - nsql::exec(insert_dep_st, - package_rowid, - dep.name, - iv_1.low.to_string(), - iv_1.high.to_string()); - } - - auto dest_dir = pkg_dir() / info.ident.name / info.ident.version.to_string(); - auto stamp_path = dest_dir / "url.txt"; - fs::create_directories(dest_dir); - std::ofstream stamp_file{stamp_path, std::ios::binary}; - stamp_file << url; -} diff --git a/src/dds/repoman/repoman.hpp b/src/dds/repoman/repoman.hpp deleted file mode 100644 index 02ceec8b..00000000 --- a/src/dds/repoman/repoman.hpp +++ /dev/null @@ -1,73 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include -#include - -namespace dds { - -struct pkg_listing; - -struct e_init_repo { - fs::path path; -}; - -struct e_open_repo { - fs::path path; -}; - -struct e_init_repo_db { - fs::path path; -}; - -struct e_open_repo_db { - fs::path path; -}; - -struct e_repo_import_targz { - fs::path path; -}; - -struct e_repo_delete_path { - fs::path path; -}; - -class repo_manager { - neo::sqlite3::database _db; - mutable neo::sqlite3::statement_cache _stmts{_db}; - fs::path _root; - - explicit repo_manager(path_ref root, neo::sqlite3::database db) - : _db(std::move(db)) - , _root(root) {} - -public: - repo_manager(repo_manager&&) = default; - - static repo_manager create(path_ref directory, std::optional name); - static repo_manager open(path_ref directory); - - auto pkg_dir() const noexcept { return _root / "pkg"; } - path_ref root() const noexcept { return _root; } - std::string name() const noexcept; - - void import_targz(path_ref tgz_path); - void delete_package(pkg_id id); - void add_pkg(const pkg_listing& info, std::string_view url); - - auto all_packages() const noexcept { - using namespace neo::sqlite3::literals; - auto& st = _stmts("SELECT name, version FROM dds_repo_packages"_sql); - auto tups = neo::sqlite3::iter_tuples(st); - return tups | ranges::views::transform([](auto&& pair) { - auto [name, version] = pair; - return pkg_id{name, semver::version::parse(version)}; - }); - } -}; - -} // namespace dds diff --git a/src/dds/repoman/repoman.test.cpp b/src/dds/repoman/repoman.test.cpp deleted file mode 100644 index 19ffc8c1..00000000 --- a/src/dds/repoman/repoman.test.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#include - -#include -#include - -#include - -#include - -namespace { - -const auto THIS_FILE = dds::fs::canonical(__FILE__); -const auto THIS_DIR = THIS_FILE.parent_path(); -const auto REPO_ROOT = (THIS_DIR / "../../../").lexically_normal(); -const auto DATA_DIR = REPO_ROOT / "data"; - -struct tmp_repo { - dds::temporary_dir tempdir = dds::temporary_dir::create(); - dds::repo_manager repo = dds::repo_manager::create(tempdir.path(), "test-repo"); -}; - -} // namespace - -TEST_CASE_METHOD(tmp_repo, "Open and import into a repository") { - auto neo_url_tgz = DATA_DIR / "neo-url@0.2.1.tar.gz"; - repo.import_targz(neo_url_tgz); - CHECK(dds::fs::is_directory(repo.pkg_dir() / "neo-url/")); - CHECK(dds::fs::is_regular_file(repo.pkg_dir() / "neo-url/0.2.1/sdist.tar.gz")); - CHECK_THROWS_AS(repo.import_targz(neo_url_tgz), neo::sqlite3::constraint_unique_error); - repo.delete_package(dds::pkg_id::parse("neo-url@0.2.1")); - CHECK_FALSE(dds::fs::is_regular_file(repo.pkg_dir() / "neo-url/0.2.1/sdist.tar.gz")); - CHECK_FALSE(dds::fs::is_directory(repo.pkg_dir() / "neo-url")); - CHECK_THROWS_AS(repo.delete_package(dds::pkg_id::parse("neo-url@0.2.1")), std::system_error); - CHECK_NOTHROW(repo.import_targz(neo_url_tgz)); -} - -TEST_CASE_METHOD(tmp_repo, "Add a package directly") { - dds::pkg_listing info{ - .ident = dds::pkg_id::parse("foo@1.2.3"), - .deps = {}, - .description = "Something", - .remote_pkg = {}, - }; - repo.add_pkg(info, "http://example.com"); - CHECK_THROWS_AS(repo.add_pkg(info, "https://example.com"), - neo::sqlite3::constraint_unique_error); - repo.delete_package(dds::pkg_id::parse("foo@1.2.3")); -} diff --git a/src/dds/sdist/dist.cpp b/src/dds/sdist/dist.cpp deleted file mode 100644 index bbdda329..00000000 --- a/src/dds/sdist/dist.cpp +++ /dev/null @@ -1,157 +0,0 @@ -#include "./dist.hpp" - -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include - -using namespace dds; - -namespace { - -void sdist_export_file(path_ref out_root, path_ref in_root, path_ref filepath) { - auto relpath = fs::relative(filepath, in_root); - dds_log(debug, "Export file {}", relpath.string()); - auto dest = out_root / relpath; - fs::create_directories(fs::absolute(dest).parent_path()); - fs::copy(filepath, dest); -} - -void sdist_copy_library(path_ref out_root, const library_root& lib, const sdist_params& params) { - auto sources_to_keep = // - lib.all_sources() // - | ranges::views::filter([&](const source_file& sf) { - if (sf.kind == source_kind::app && params.include_apps) { - return true; - } - if (sf.kind == source_kind::source || sf.kind == source_kind::header) { - return true; - } - if (sf.kind == source_kind::test && params.include_tests) { - return true; - } - return false; - }) // - | ranges::to_vector; - - ranges::sort(sources_to_keep, std::less<>(), [](auto&& s) { return s.path; }); - - auto lib_man_path = library_manifest::find_in_directory(lib.path()); - if (!lib_man_path) { - throw_user_error( - "Each library root in a source distribution requires a library manifest (Expected a " - "library manifest in [{}])", - lib.path().string()); - } - sdist_export_file(out_root, params.project_dir, *lib_man_path); - - dds_log(info, "sdist: Export library from {}", lib.path().string()); - fs::create_directories(out_root); - for (const auto& source : sources_to_keep) { - sdist_export_file(out_root, params.project_dir, source.path); - } -} - -} // namespace - -sdist dds::create_sdist(const sdist_params& params) { - auto dest = fs::absolute(params.dest_path); - if (fs::exists(dest)) { - if (!params.force) { - throw_user_error("Destination path '{}' already exists", - dest.string()); - } - } - - auto tempdir = temporary_dir::create(); - create_sdist_in_dir(tempdir.path(), params); - if (fs::exists(dest) && params.force) { - fs::remove_all(dest); - } - fs::create_directories(fs::absolute(dest).parent_path()); - safe_rename(tempdir.path(), dest); - dds_log(info, "Source distribution created in {}", dest.string()); - return sdist::from_directory(dest); -} - -void dds::create_sdist_targz(path_ref filepath, const sdist_params& params) { - if (fs::exists(filepath)) { - if (!params.force) { - throw_user_error("Destination path '{}' already exists", - filepath.string()); - } - } - - auto tempdir = temporary_dir::create(); - dds_log(debug, "Generating source distribution in {}", tempdir.path().string()); - create_sdist_in_dir(tempdir.path(), params); - fs::create_directories(fs::absolute(filepath).parent_path()); - neo::compress_directory_targz(tempdir.path(), filepath); -} - -sdist dds::create_sdist_in_dir(path_ref out, const sdist_params& params) { - auto libs = collect_libraries(params.project_dir); - - for (const library_root& lib : libs) { - sdist_copy_library(out, lib, params); - } - - auto man_path = package_manifest::find_in_directory(params.project_dir); - if (!man_path) { - throw_user_error( - "Creating a source distribution requires a package.json5 file for the project " - "(Expected manifest in [{}])", - params.project_dir.string()); - } - - auto pkg_man = package_manifest::load_from_file(*man_path); - sdist_export_file(out, params.project_dir, *man_path); - return sdist::from_directory(out); -} - -sdist sdist::from_directory(path_ref where) { - auto pkg_man = package_manifest::load_from_directory(where); - // Code paths should only call here if they *know* that the sdist is valid - if (!pkg_man) { - throw_user_error( - "The given directory [{}] does not contain a package manifest file. All source " - "distribution directories are required to contain a package manifest.", - where.string()); - } - return sdist{pkg_man.value(), where}; -} - -temporary_sdist dds::expand_sdist_targz(path_ref targz_path) { - neo_assertion_breadcrumbs("Expanding sdist targz file", targz_path.string()); - auto infile = open(targz_path, std::ios::binary | std::ios::in); - return expand_sdist_from_istream(infile, targz_path.string()); -} - -temporary_sdist dds::expand_sdist_from_istream(std::istream& is, std::string_view input_name) { - auto tempdir = temporary_dir::create(); - dds_log(debug, - "Expanding source distribution content from [{}] into [{}]", - input_name, - tempdir.path().string()); - fs::create_directories(tempdir.path()); - neo::expand_directory_targz({.destination_directory = tempdir.path(), .input_name = input_name}, - is); - return {tempdir, sdist::from_directory(tempdir.path())}; -} - -temporary_sdist dds::download_expand_sdist_targz(std::string_view url_str) { - auto remote = http_remote_pkg::from_url(neo::url::parse(url_str)); - auto tempdir = temporary_dir::create(); - remote.get_raw_directory(tempdir.path()); - return {tempdir, sdist::from_directory(tempdir.path())}; -} diff --git a/src/dds/sdist/dist.hpp b/src/dds/sdist/dist.hpp deleted file mode 100644 index 81aa5780..00000000 --- a/src/dds/sdist/dist.hpp +++ /dev/null @@ -1,52 +0,0 @@ -#pragma once - -#include - -#include -#include -#include - -namespace dds { - -struct sdist_params { - fs::path project_dir; - fs::path dest_path; - bool force = false; - bool include_apps = false; - bool include_tests = false; -}; - -struct sdist { - package_manifest manifest; - fs::path path; - - sdist(package_manifest man, path_ref path_) - : manifest(std::move(man)) - , path(path_) {} - - static sdist from_directory(path_ref p); -}; - -struct temporary_sdist { - temporary_dir tmpdir; - struct sdist sdist; -}; - -inline constexpr struct sdist_compare_t { - bool operator()(const sdist& lhs, const sdist& rhs) const { - return lhs.manifest.id < rhs.manifest.id; - } - bool operator()(const sdist& lhs, const pkg_id& rhs) const { return lhs.manifest.id < rhs; } - bool operator()(const pkg_id& lhs, const sdist& rhs) const { return lhs < rhs.manifest.id; } - using is_transparent = int; -} sdist_compare; - -sdist create_sdist(const sdist_params&); -sdist create_sdist_in_dir(path_ref, const sdist_params&); -void create_sdist_targz(path_ref, const sdist_params&); - -temporary_sdist expand_sdist_targz(path_ref targz); -temporary_sdist expand_sdist_from_istream(std::istream&, std::string_view input_name); -temporary_sdist download_expand_sdist_targz(std::string_view); - -} // namespace dds diff --git a/src/dds/sdist/library/manifest.cpp b/src/dds/sdist/library/manifest.cpp deleted file mode 100644 index 39f37757..00000000 --- a/src/dds/sdist/library/manifest.cpp +++ /dev/null @@ -1,88 +0,0 @@ -#include "./manifest.hpp" - -#include -#include -#include - -#include -#include -#include - -using namespace dds; - -library_manifest library_manifest::load_from_file(path_ref fpath) { - auto content = slurp_file(fpath); - auto data = json5::parse_data(content); - - if (!data.is_object()) { - throw_user_error("Root value must be an object"); - } - - library_manifest lib; - using namespace semester::decompose_ops; - auto res = semester::decompose( // - data, - try_seq{require_type{ - "The root of the library manifest must be an object (mapping)"}, - mapping{ - if_key{"name", - require_type{"`name` must be a string"}, - put_into{lib.name}}, - if_key{"uses", - require_type{ - "`uses` must be an array of usage requirements"}, - for_each{ - require_type{"`uses` elements must be strings"}, - [&](auto&& uses) { - lib.uses.push_back(lm::split_usage_string(uses.as_string())); - return semester::dc_accept; - }, - }}, - if_key{"links", - require_type{ - "`links` must be an array of usage requirements"}, - for_each{ - require_type{"`links` elements must be strings"}, - [&](auto&& links) { - lib.links.push_back(lm::split_usage_string(links.as_string())); - return semester::dc_accept; - }, - }}, - }}); - auto rej = std::get_if(&res); - if (rej) { - throw_user_error(rej->message); - } - - if (lib.name.empty()) { - throw_user_error( - "The 'name' field is required (Reading library manifest [{}])", fpath.string()); - } - - return lib; -} - -std::optional library_manifest::find_in_directory(path_ref dirpath) { - auto fnames = { - "library.json5", - "library.jsonc", - "library.json", - }; - for (auto c : fnames) { - auto cand = dirpath / c; - if (fs::is_regular_file(cand)) { - return cand; - } - } - - return std::nullopt; -} - -std::optional library_manifest::load_from_directory(path_ref dirpath) { - auto found = find_in_directory(dirpath); - if (!found.has_value()) { - return std::nullopt; - } - - return load_from_file(*found); -} \ No newline at end of file diff --git a/src/dds/sdist/library/manifest.hpp b/src/dds/sdist/library/manifest.hpp deleted file mode 100644 index 6d88a814..00000000 --- a/src/dds/sdist/library/manifest.hpp +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include - -#include - -#include - -namespace dds { - -/** - * Represents the contents of a `library.json5`. This is somewhat a stripped-down - * version of lm::library, to only represent exactly the parts that we want to - * offer via `library.json5`. - */ -struct library_manifest { - /// The name of the library - std::string name; - /// The libraries that the owning library "uses" - std::vector uses; - /// The libraries that the owning library must be linked with - std::vector links; - - /** - * Load the library manifest from an existing file - */ - static library_manifest load_from_file(path_ref); - - /** - * Find a library manifest within a directory. This will search for a few - * file candidates and return the result from the first matching. If none - * match, it will return nullopt. - */ - static std::optional find_in_directory(path_ref); - static std::optional load_from_directory(path_ref); -}; - -} // namespace dds diff --git a/src/dds/sdist/library/root.cpp b/src/dds/sdist/library/root.cpp deleted file mode 100644 index bf2f95b3..00000000 --- a/src/dds/sdist/library/root.cpp +++ /dev/null @@ -1,117 +0,0 @@ -#include - -#include -#include -#include -#include -#include - -#include -#include -#include - -using namespace dds; - -namespace { - -auto collect_pf_sources(path_ref path) { - dds_log(debug, "Scanning for sources in {}", path.string()); - auto include_dir = source_root{path / "include"}; - auto src_dir = source_root{path / "src"}; - - source_list sources; - - if (include_dir.exists()) { - if (!fs::is_directory(include_dir.path)) { - throw_user_error( - "The `include` at the library root [in {}] is a non-directory file", path.string()); - } - auto inc_sources = include_dir.collect_sources(); - // Drop any source files we found within `include/` - erase_if(sources, [&](auto& info) { - if (info.kind != source_kind::header) { - dds_log(warn, - "Source file in `include` will not be compiled: {}", - info.path.string()); - return true; - } - return false; - }); - extend(sources, inc_sources); - } - - if (src_dir.exists()) { - if (!fs::is_directory(src_dir.path)) { - throw_user_error( - "The `src` at the library root [in {}] is a non-directory file", path.string()); - } - auto src_sources = src_dir.collect_sources(); - extend(sources, src_sources); - } - - dds_log(debug, "Found {} source files", sources.size()); - return sources; -} - -} // namespace - -library_root library_root::from_directory(path_ref lib_dir) { - assert(lib_dir.is_absolute()); - auto sources = collect_pf_sources(lib_dir); - - library_manifest man; - man.name = lib_dir.filename().string(); - auto found = library_manifest::find_in_directory(lib_dir); - if (found) { - man = library_manifest::load_from_file(*found); - } - - auto lib = library_root(lib_dir, std::move(sources), std::move(man)); - - return lib; -} - -fs::path library_root::public_include_dir() const noexcept { - auto inc_dir = include_source_root(); - if (inc_dir.exists()) { - return inc_dir.path; - } - return src_source_root().path; -} - -fs::path library_root::private_include_dir() const noexcept { return src_source_root().path; } - -shared_compile_file_rules library_root::base_compile_rules() const noexcept { - auto inc_dir = include_source_root(); - auto src_dir = this->src_source_root(); - shared_compile_file_rules ret; - if (inc_dir.exists()) { - ret.include_dirs().push_back(inc_dir.path); - } - if (src_dir.exists()) { - ret.include_dirs().push_back(src_dir.path); - } - return ret; -} - -auto has_library_dirs - = [](path_ref dir) { return fs::exists(dir / "src") || fs::exists(dir / "include"); }; - -std::vector dds::collect_libraries(path_ref root) { - std::vector ret; - if (has_library_dirs(root)) { - ret.emplace_back(library_root::from_directory(fs::canonical(root))); - } - - auto pf_libs_dir = root / "libs"; - - if (fs::is_directory(pf_libs_dir)) { - extend(ret, - fs::directory_iterator(pf_libs_dir) // - | neo::lref // - | ranges::views::filter(has_library_dirs) // - | ranges::views::transform( - [&](auto p) { return library_root::from_directory(fs::canonical(p)); })); - } - return ret; -} diff --git a/src/dds/sdist/library/root.hpp b/src/dds/sdist/library/root.hpp deleted file mode 100644 index 1a427c7b..00000000 --- a/src/dds/sdist/library/root.hpp +++ /dev/null @@ -1,93 +0,0 @@ -#pragma once - -#include "./manifest.hpp" - -#include "../file.hpp" -#include "../root.hpp" -#include - -#include - -namespace dds { - -/** - * Represents a library that exists on the filesystem - */ -class library_root { - // The path containing the source directories for this library - fs::path _path; - // The sources that are part of this library - source_list _sources; - // The library manifest associated with this library (may be generated) - library_manifest _man; - - // Private constructor. Use named constructor `from_directory`, which will build - // the construct arguments approperiately - library_root(path_ref dir, source_list&& src, library_manifest&& man) - : _path(dir) - , _sources(std::move(src)) - , _man(std::move(man)) {} - -public: - /** - * Create a library object that refers to the library contained at the given - * directory path. This will load the sources and manifest properly and - * return the resulting library object. - */ - static library_root from_directory(path_ref); - - /** - * Obtain the manifest for this library - */ - const library_manifest& manifest() const noexcept { return _man; } - - /** - * The `src/` directory for this library. - */ - source_root src_source_root() const noexcept { return source_root{path() / "src"}; } - - /** - * The `include/` directory for this library - */ - source_root include_source_root() const noexcept { return source_root{path() / "include"}; } - - /** - * The root path for this library (parent of `src/` and `include/`, if present) - */ - path_ref path() const noexcept { return _path; } - - /** - * The directory that should be considered the "public" include directory. - * Dependees that want to use this library should add this to their #include - * search path. - */ - fs::path public_include_dir() const noexcept; - - /** - * The directory that contains the "private" heders for this libary. This - * directory should be added to the search path of the library when it is - * being built, but NOT to the search path of the dependees. - */ - fs::path private_include_dir() const noexcept; - - /** - * Get the sources that this library contains - */ - const source_list& all_sources() const noexcept { return _sources; } - - /** - * Generate a compile rules object that should be used when compiling - * this library. - */ - shared_compile_file_rules base_compile_rules() const noexcept; -}; - -/** - * Given the root source directory of a project/package/sdist, collect all of - * the libraries that it contains. There may be a library directly in `where`, - * but there might also be libraries in `where/libs`. This function will find - * them all. - */ -std::vector collect_libraries(path_ref where); - -} // namespace dds \ No newline at end of file diff --git a/src/dds/sdist/package.cpp b/src/dds/sdist/package.cpp deleted file mode 100644 index 230cbc6a..00000000 --- a/src/dds/sdist/package.cpp +++ /dev/null @@ -1,156 +0,0 @@ -#include "./package.hpp" - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include - -using namespace dds; - -namespace { - -using require_obj = semester::require_type; -using require_array = semester::require_type; -using require_str = semester::require_type; - -package_manifest parse_json(const json5::data& data, std::string_view fpath) { - package_manifest ret; - - using namespace semester::walk_ops; - auto push_depends_obj_kv = [&](std::string key, auto&& dat) { - dependency pending_dep; - if (!dat.is_string()) { - return walk.reject("Dependency object values should be strings"); - } - try { - auto rng = semver::range::parse_restricted(dat.as_string()); - dependency dep{std::move(key), {rng.low(), rng.high()}}; - ret.dependencies.push_back(std::move(dep)); - } catch (const semver::invalid_range&) { - throw_user_error( - "Invalid version range string '{}' in dependency declaration for " - "'{}'", - dat.as_string(), - key); - } - return walk.accept; - }; - - walk(data, - require_obj{"Root of package manifest should be a JSON object"}, - mapping{ - if_key{"$schema", just_accept}, - required_key{"name", - "A string 'name' is required", - require_str{"'name' must be a string"}, - put_into{ret.id.name}}, - required_key{"namespace", - "A string 'namespace' is a required ", - require_str{"'namespace' must be a string"}, - put_into{ret.namespace_}}, - required_key{"version", - "A 'version' string is requried", - require_str{"'version' must be a string"}, - put_into{ret.id.version, - [](std::string s) { return semver::version::parse(s); }}}, - if_key{"depends", - [&](auto&& dat) { - if (dat.is_object()) { - dds_log(warn, - "{}: Using a JSON object for 'depends' is deprecated. Use an " - "array of strings instead.", - fpath); - return mapping{push_depends_obj_kv}(dat); - } else if (dat.is_array()) { - return for_each{put_into{std::back_inserter(ret.dependencies), - [](const std::string& depstr) { - return dependency::parse_depends_string( - depstr); - }}}(dat); - } else { - return walk.reject( - "'depends' should be an array of dependency strings"); - } - }}, - if_key{"test_driver", - require_str{"'test_driver' must be a string"}, - put_into{ret.test_driver, - [](std::string const& td_str) { - if (td_str == "Catch-Main") { - return test_lib::catch_main; - } else if (td_str == "Catch") { - return test_lib::catch_; - } else { - auto dym = *did_you_mean(td_str, {"Catch-Main", "Catch"}); - throw_user_error( - "Unknown 'test_driver' '{}' (Did you mean '{}'?)", - td_str, - dym); - } - }}}, - }); - return ret; -} - -} // namespace - -package_manifest package_manifest::load_from_file(const fs::path& fpath) { - auto content = slurp_file(fpath); - return load_from_json5_str(content, fpath.string()); -} - -package_manifest package_manifest::load_from_json5_str(std::string_view content, - std::string_view input_name) { - try { - auto data = json5::parse_data(content); - return parse_json(data, input_name); - } catch (const semester::walk_error& e) { - throw_user_error(e.what()); - } catch (const json5::parse_error& err) { - BOOST_LEAF_THROW_EXCEPTION(user_error( - "Invalid package manifest JSON5 document"), - err, - boost::leaf::e_file_name{std::string(input_name)}); - } -} - -result package_manifest::find_in_directory(path_ref dirpath) { - auto cands = { - "package.json5", - "package.jsonc", - "package.json", - }; - for (auto c : cands) { - auto cand = dirpath / c; - std::error_code ec; - if (fs::is_regular_file(cand, ec)) { - return cand; - } - if (ec != std::errc::no_such_file_or_directory) { - return boost::leaf:: - new_error(ec, - DDS_E_ARG(e_human_message{ - "Failed to check for package manifest in project directory"}), - DDS_E_ARG(boost::leaf::e_file_name{cand.string()})); - } - } - - return boost::leaf::new_error(std::errc::no_such_file_or_directory, - DDS_E_ARG( - e_human_message{"Expected to find a package manifest file"}), - DDS_E_ARG(e_missing_file{dirpath / "package.json5"})); -} - -result package_manifest::load_from_directory(path_ref dirpath) { - BOOST_LEAF_AUTO(found, find_in_directory(dirpath)); - return load_from_file(found); -} diff --git a/src/dds/sdist/package.hpp b/src/dds/sdist/package.hpp deleted file mode 100644 index 78919cfd..00000000 --- a/src/dds/sdist/package.hpp +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include -#include -#include - -namespace dds { - -/** - * Possible values for test_driver in a package.json5 - */ -enum class test_lib { - catch_, - catch_main, -}; - -/** - * Struct representing the contents of a `packaeg.dds` file. - */ -struct package_manifest { - /// The package ID, as determined by `Name` and `Version` together - dds::pkg_id id; - /// The declared `Namespace` of the package. This directly corresponds with the libman Namespace - std::string namespace_; - /// The `test_driver` that this package declares, or `nullopt` if absent. - std::optional test_driver; - /// The dependencies declared with the `Depends` fields, if any. - std::vector dependencies; - - /** - * Load a package manifest from a file on disk. - */ - static package_manifest load_from_file(path_ref); - /** - * @brief Load a package manifest from an in-memory string - */ - static package_manifest load_from_json5_str(std::string_view, std::string_view input_name); - - /** - * Find a package manifest contained within a directory. This will search - * for a few file candidates and return the result from the first matching. - * If none match, it will return nullopt. - */ - static result find_in_directory(path_ref); - static result load_from_directory(path_ref); -}; - -} // namespace dds \ No newline at end of file diff --git a/src/dds/sdist/root.cpp b/src/dds/sdist/root.cpp deleted file mode 100644 index 739704b0..00000000 --- a/src/dds/sdist/root.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#include "./root.hpp" - -#include -#include -#include -#include - -using namespace dds; - -std::vector source_root::collect_sources() const { - using namespace ranges::views; - // Collect all source files from the directory - return // - fs::recursive_directory_iterator(path) // - | neo::lref // - | filter([](auto&& entry) { return entry.is_regular_file(); }) // - | transform([&](auto&& entry) { return source_file::from_path(entry, path); }) // - // source_file::from_path returns an optional. Drop nulls - | filter([](auto&& opt) { return opt.has_value(); }) // - | transform([](auto&& opt) { return *opt; }) // - | ranges::to_vector; -} diff --git a/src/dds/solve/solve.cpp b/src/dds/solve/solve.cpp deleted file mode 100644 index 39cc629c..00000000 --- a/src/dds/solve/solve.cpp +++ /dev/null @@ -1,173 +0,0 @@ -#include "./solve.hpp" - -#include -#include - -#include - -#include -#include - -#include - -using namespace dds; - -namespace { - -struct req_type { - dependency dep; - - using req_ref = const req_type&; - - bool implied_by(req_ref other) const noexcept { - return dep.versions.contains(other.dep.versions); - } - - bool excludes(req_ref other) const noexcept { - return dep.versions.disjoint(other.dep.versions); - } - - std::optional intersection(req_ref other) const noexcept { - auto range = dep.versions.intersection(other.dep.versions); - if (range.empty()) { - return std::nullopt; - } - return req_type{dependency{dep.name, std::move(range)}}; - } - - std::optional union_(req_ref other) const noexcept { - auto range = dep.versions.union_(other.dep.versions); - if (range.empty()) { - return std::nullopt; - } - return req_type{dependency{dep.name, std::move(range)}}; - } - - std::optional difference(req_ref other) const noexcept { - auto range = dep.versions.difference(other.dep.versions); - if (range.empty()) { - return std::nullopt; - } - return req_type{dependency{dep.name, std::move(range)}}; - } - - auto key() const noexcept { return dep.name; } - - friend bool operator==(req_ref lhs, req_ref rhs) noexcept { - return lhs.dep.name == rhs.dep.name && lhs.dep.versions == rhs.dep.versions; - } - - friend std::ostream& operator<<(std::ostream& out, req_ref self) noexcept { - out << self.dep.to_string(); - return out; - } -}; - -auto as_pkg_id(const req_type& req) { - const version_range_set& versions = req.dep.versions; - assert(versions.num_intervals() == 1); - return pkg_id{req.dep.name, (*versions.iter_intervals().begin()).low}; -} - -struct solver_provider { - pkg_id_provider_fn& pkgs_for_name; - deps_provider_fn& deps_for_pkg; - - mutable std::map> pkgs_by_name = {}; - - std::optional best_candidate(const req_type& req) const { - dds_log(debug, "Find best candidate of {}", req.dep.to_string()); - // Look up in the cachce for the packages we have with the given name - auto found = pkgs_by_name.find(req.dep.name); - if (found == pkgs_by_name.end()) { - // If it isn't there, insert an entry in the cache - found = pkgs_by_name.emplace(req.dep.name, pkgs_for_name(req.dep.name)).first; - } - // Find the first package with the version contained by the ranges in the requirement - auto& for_name = found->second; - auto cand = std::find_if(for_name.cbegin(), for_name.cend(), [&](const pkg_id& pk) { - return req.dep.versions.contains(pk.version); - }); - if (cand == for_name.cend()) { - dds_log(debug, "No candidate for requirement {}", req.dep.to_string()); - return std::nullopt; - } - dds_log(debug, "Select candidate {}", cand->to_string()); - return req_type{dependency{cand->name, {cand->version, cand->version.next_after()}}}; - } - - std::vector requirements_of(const req_type& req) const { - dds_log(trace, - "Lookup requirements of {}@{}", - req.key(), - (*req.dep.versions.iter_intervals().begin()).low.to_string()); - auto pk_id = as_pkg_id(req); - auto deps = deps_for_pkg(pk_id); - return deps // - | ranges::views::transform([](const dependency& dep) { return req_type{dep}; }) // - | ranges::to_vector; - } -}; - -using solve_fail_exc = pubgrub::solve_failure_type_t; - -struct explainer { - std::stringstream strm; - bool at_head = true; - - void put(pubgrub::explain::no_solution) { strm << "Dependencies cannot be satisfied"; } - - void put(pubgrub::explain::dependency dep) { - strm << dep.dependent << " requires " << dep.dependency; - } - - void put(pubgrub::explain::unavailable un) { - strm << un.requirement << " is not available"; - } - - void put(pubgrub::explain::conflict cf) { - strm << cf.a << " conflicts with " << cf.b; - } - - void put(pubgrub::explain::needed req) { strm << req.requirement << " is required"; } - - void put(pubgrub::explain::disallowed dis) { - strm << dis.requirement << " cannot be used"; - } - - template - void operator()(pubgrub::explain::premise pr) { - strm.str(""); - put(pr.value); - dds_log(error, "{} {},", at_head ? "┌─ Given that" : "│ and", strm.str()); - at_head = false; - } - - template - void operator()(pubgrub::explain::conclusion cncl) { - at_head = true; - strm.str(""); - put(cncl.value); - dds_log(error, "╘═ then {}.", strm.str()); - } - - void operator()(pubgrub::explain::separator) { dds_log(error, ""); } -}; - -} // namespace - -std::vector dds::solve(const std::vector& deps, - pkg_id_provider_fn pkgs_prov, - deps_provider_fn deps_prov) { - auto wrap_req - = deps | ranges::views::transform([](const dependency& dep) { return req_type{dep}; }); - - try { - auto solution = pubgrub::solve(wrap_req, solver_provider{pkgs_prov, deps_prov}); - return solution | ranges::views::transform(as_pkg_id) | ranges::to_vector; - } catch (const solve_fail_exc& failure) { - dds_log(error, "Dependency resolution has failed! Explanation:"); - pubgrub::generate_explaination(failure, explainer()); - throw_user_error(); - } -} diff --git a/src/dds/solve/solve.hpp b/src/dds/solve/solve.hpp deleted file mode 100644 index b44754ec..00000000 --- a/src/dds/solve/solve.hpp +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include -#include - -#include - -namespace dds { - -using pkg_id_provider_fn = std::function(std::string_view)>; -using deps_provider_fn = std::function(const pkg_id& pk)>; - -std::vector -solve(const std::vector& deps, pkg_id_provider_fn, deps_provider_fn); - -} // namespace dds diff --git a/src/dds/toolchain/from_json.test.cpp b/src/dds/toolchain/from_json.test.cpp deleted file mode 100644 index 31331f5f..00000000 --- a/src/dds/toolchain/from_json.test.cpp +++ /dev/null @@ -1,192 +0,0 @@ -#include - -#include - -#include - -namespace { -void check_tc_compile(std::string_view tc_content, - std::string_view expected_compile, - std::string_view expected_compile_warnings, - std::string_view expected_ar, - std::string_view expected_exe) { - auto tc = dds::parse_toolchain_json5(tc_content); - - dds::compile_file_spec cf; - cf.source_path = "foo.cpp"; - cf.out_path = "foo.o"; - auto cf_cmd = tc.create_compile_command(cf, dds::fs::current_path(), dds::toolchain_knobs{}); - auto cf_cmd_str = dds::quote_command(cf_cmd.command); - CHECK(cf_cmd_str == expected_compile); - - cf.enable_warnings = true; - cf_cmd = tc.create_compile_command(cf, dds::fs::current_path(), dds::toolchain_knobs{}); - cf_cmd_str = dds::quote_command(cf_cmd.command); - CHECK(cf_cmd_str == expected_compile_warnings); - - dds::archive_spec ar_spec; - ar_spec.input_files.push_back("foo.o"); - ar_spec.input_files.push_back("bar.o"); - ar_spec.out_path = "stuff.a"; - auto ar_cmd - = tc.create_archive_command(ar_spec, dds::fs::current_path(), dds::toolchain_knobs{}); - auto ar_cmd_str = dds::quote_command(ar_cmd); - CHECK(ar_cmd_str == expected_ar); - - dds::link_exe_spec exe_spec; - exe_spec.inputs.push_back("foo.o"); - exe_spec.inputs.push_back("bar.a"); - exe_spec.output = "meow.exe"; - auto exe_cmd = tc.create_link_executable_command(exe_spec, - dds::fs::current_path(), - dds::toolchain_knobs{}); - auto exe_cmd_str = dds::quote_command(exe_cmd); - CHECK(exe_cmd_str == expected_exe); -} - -} // namespace - -TEST_CASE("Generating toolchain commands") { - check_tc_compile("{compiler_id: 'gnu'}", - "g++ -fPIC -pthread -MD -MF foo.o.d -MQ foo.o -c foo.cpp -ofoo.o", - "g++ -fPIC -pthread -Wall -Wextra -Wpedantic -Wconversion " - "-MD -MF foo.o.d -MQ foo.o -c foo.cpp -ofoo.o", - "ar rcs stuff.a foo.o bar.o", - "g++ -fPIC foo.o bar.a -pthread -omeow.exe"); - - check_tc_compile("{compiler_id: 'gnu', debug: true}", - "g++ -g -fPIC -pthread -MD -MF foo.o.d -MQ foo.o -c foo.cpp -ofoo.o", - "g++ -g -fPIC -pthread -Wall -Wextra -Wpedantic -Wconversion " - "-MD -MF foo.o.d -MQ foo.o -c foo.cpp -ofoo.o", - "ar rcs stuff.a foo.o bar.o", - "g++ -fPIC foo.o bar.a -pthread -omeow.exe -g"); - - check_tc_compile("{compiler_id: 'gnu', debug: true, optimize: true}", - "g++ -O2 -g -fPIC -pthread -MD -MF foo.o.d -MQ foo.o -c foo.cpp " - "-ofoo.o", - "g++ -O2 -g -fPIC -pthread -Wall -Wextra -Wpedantic -Wconversion " - "-MD -MF foo.o.d -MQ foo.o -c foo.cpp -ofoo.o", - "ar rcs stuff.a foo.o bar.o", - "g++ -fPIC foo.o bar.a -pthread -omeow.exe -O2 -g"); - - check_tc_compile( - "{compiler_id: 'gnu', debug: 'split', optimize: true}", - "g++ -O2 -g -gsplit-dwarf -fPIC -pthread -MD -MF foo.o.d -MQ foo.o -c foo.cpp -ofoo.o", - "g++ -O2 -g -gsplit-dwarf -fPIC -pthread -Wall -Wextra -Wpedantic -Wconversion -MD -MF " - "foo.o.d -MQ foo.o -c foo.cpp -ofoo.o", - "ar rcs stuff.a foo.o bar.o", - "g++ -fPIC foo.o bar.a -pthread -omeow.exe -O2 -g -gsplit-dwarf"); - - check_tc_compile("{compiler_id: 'msvc'}", - "cl.exe /MT /EHsc /nologo /permissive- /showIncludes /c foo.cpp /Fofoo.o", - "cl.exe /MT /EHsc /nologo /permissive- /W4 /showIncludes /c foo.cpp /Fofoo.o", - "lib /nologo /OUT:stuff.a foo.o bar.o", - "cl.exe /nologo /EHsc foo.o bar.a /Femeow.exe /MT"); - - check_tc_compile( - "{compiler_id: 'msvc', debug: true}", - "cl.exe /MTd /Z7 /EHsc /nologo /permissive- /showIncludes /c foo.cpp /Fofoo.o", - "cl.exe /MTd /Z7 /EHsc /nologo /permissive- /W4 /showIncludes /c foo.cpp /Fofoo.o", - "lib /nologo /OUT:stuff.a foo.o bar.o", - "cl.exe /nologo /EHsc foo.o bar.a /Femeow.exe /MTd /Z7"); - - check_tc_compile( - "{compiler_id: 'msvc', debug: 'embedded'}", - "cl.exe /MTd /Z7 /EHsc /nologo /permissive- /showIncludes /c foo.cpp /Fofoo.o", - "cl.exe /MTd /Z7 /EHsc /nologo /permissive- /W4 /showIncludes /c foo.cpp /Fofoo.o", - "lib /nologo /OUT:stuff.a foo.o bar.o", - "cl.exe /nologo /EHsc foo.o bar.a /Femeow.exe /MTd /Z7"); - - check_tc_compile( - "{compiler_id: 'msvc', debug: 'split'}", - "cl.exe /MTd /Zi /FS /EHsc /nologo /permissive- /showIncludes /c foo.cpp /Fofoo.o", - "cl.exe /MTd /Zi /FS /EHsc /nologo /permissive- /W4 /showIncludes /c foo.cpp /Fofoo.o", - "lib /nologo /OUT:stuff.a foo.o bar.o", - "cl.exe /nologo /EHsc foo.o bar.a /Femeow.exe /MTd /Zi /FS"); - - check_tc_compile( - "{compiler_id: 'msvc', flags: '-DFOO'}", - "cl.exe /MT /EHsc /nologo /permissive- /showIncludes /c foo.cpp /Fofoo.o -DFOO", - "cl.exe /MT /EHsc /nologo /permissive- /W4 /showIncludes /c foo.cpp /Fofoo.o -DFOO", - "lib /nologo /OUT:stuff.a foo.o bar.o", - "cl.exe /nologo /EHsc foo.o bar.a /Femeow.exe /MT"); - - check_tc_compile("{compiler_id: 'msvc', runtime: {static: false}}", - "cl.exe /MD /EHsc /nologo /permissive- /showIncludes /c foo.cpp /Fofoo.o", - "cl.exe /MD /EHsc /nologo /permissive- /W4 /showIncludes /c foo.cpp /Fofoo.o", - "lib /nologo /OUT:stuff.a foo.o bar.o", - "cl.exe /nologo /EHsc foo.o bar.a /Femeow.exe /MD"); - - check_tc_compile( - "{compiler_id: 'msvc', runtime: {static: false}, debug: true}", - "cl.exe /MDd /Z7 /EHsc /nologo /permissive- /showIncludes /c foo.cpp /Fofoo.o", - "cl.exe /MDd /Z7 /EHsc /nologo /permissive- /W4 /showIncludes /c foo.cpp /Fofoo.o", - "lib /nologo /OUT:stuff.a foo.o bar.o", - "cl.exe /nologo /EHsc foo.o bar.a /Femeow.exe /MDd /Z7"); - - check_tc_compile("{compiler_id: 'msvc', runtime: {static: false, debug: true}}", - "cl.exe /MDd /EHsc /nologo /permissive- /showIncludes /c foo.cpp /Fofoo.o", - "cl.exe /MDd /EHsc /nologo /permissive- /W4 /showIncludes /c foo.cpp /Fofoo.o", - "lib /nologo /OUT:stuff.a foo.o bar.o", - "cl.exe /nologo /EHsc foo.o bar.a /Femeow.exe /MDd"); -} - -TEST_CASE("Manipulate a toolchain and file compilation") { - auto tc = dds::parse_toolchain_json5("{compiler_id: 'gnu'}"); - - dds::compile_file_spec cfs; - cfs.source_path = "foo.cpp"; - cfs.out_path = "foo.o"; - auto cmd = tc.create_compile_command(cfs, dds::fs::current_path(), dds::toolchain_knobs{}); - CHECK(cmd.command - == std::vector{"g++", - "-fPIC", - "-pthread", - "-MD", - "-MF", - "foo.o.d", - "-MQ", - "foo.o", - "-c", - "foo.cpp", - "-ofoo.o"}); - - cfs.definitions.push_back("FOO=BAR"); - cmd = tc.create_compile_command(cfs, - dds::fs::current_path(), - dds::toolchain_knobs{.is_tty = true}); - CHECK(cmd.command - == std::vector{"g++", - "-fPIC", - "-pthread", - "-fdiagnostics-color", - "-D", - "FOO=BAR", - "-MD", - "-MF", - "foo.o.d", - "-MQ", - "foo.o", - "-c", - "foo.cpp", - "-ofoo.o"}); - - cfs.include_dirs.push_back("fake-dir"); - cmd = tc.create_compile_command(cfs, dds::fs::current_path(), dds::toolchain_knobs{}); - CHECK(cmd.command - == std::vector{"g++", - "-fPIC", - "-pthread", - "-I", - "fake-dir", - "-D", - "FOO=BAR", - "-MD", - "-MF", - "foo.o.d", - "-MQ", - "foo.o", - "-c", - "foo.cpp", - "-ofoo.o"}); -} diff --git a/src/dds/toolchain/prep.cpp b/src/dds/toolchain/prep.cpp deleted file mode 100644 index 037fe819..00000000 --- a/src/dds/toolchain/prep.cpp +++ /dev/null @@ -1,7 +0,0 @@ -#include "./prep.hpp" - -#include - -using namespace dds; - -toolchain toolchain_prep::realize() const { return toolchain::realize(*this); } diff --git a/src/dds/usage_reqs.cpp b/src/dds/usage_reqs.cpp deleted file mode 100644 index f9864ea9..00000000 --- a/src/dds/usage_reqs.cpp +++ /dev/null @@ -1,76 +0,0 @@ -#include "./usage_reqs.hpp" - -#include -#include -#include - -#include - -#include - -using namespace dds; - -const lm::library* usage_requirement_map::get(const lm::usage& key) const noexcept { - auto found = _reqs.find(key); - if (found == _reqs.end()) { - return nullptr; - } - return &found->second; -} - -lm::library& usage_requirement_map::add(std::string ns, std::string name) { - auto pair = std::pair(library_key{ns, name}, lm::library{}); - auto [inserted, did_insert] = _reqs.try_emplace(library_key{ns, name}, lm::library()); - if (!did_insert) { - throw_user_error("More than one library is registered as `{}/{}'", - ns, - name); - } - return inserted->second; -} - -usage_requirement_map usage_requirement_map::from_lm_index(const lm::index& idx) noexcept { - usage_requirement_map ret; - for (const auto& pkg : idx.packages) { - for (const auto& lib : pkg.libraries) { - ret.add(pkg.namespace_, lib.name, lib); - } - } - return ret; -} - -std::vector usage_requirement_map::link_paths(const lm::usage& key) const { - auto req = get(key); - if (!req) { - throw_user_error("Unable to find linking requirement '{}/{}'", - key.namespace_, - key.name); - } - std::vector ret; - if (req->linkable_path) { - ret.push_back(*req->linkable_path); - } - for (const auto& dep : req->uses) { - extend(ret, link_paths(dep)); - } - for (const auto& link : req->links) { - extend(ret, link_paths(link)); - } - return ret; -} - -std::vector usage_requirement_map::include_paths(const lm::usage& usage) const { - std::vector ret; - auto lib = get(usage.namespace_, usage.name); - if (!lib) { - throw_user_error< - errc::unknown_usage_name>("Cannot find non-existent usage requirements for '{}/{}'", - usage.namespace_, - usage.name); - } - extend(ret, lib->include_paths); - for (const auto& transitive : lib->uses) { - extend(ret, include_paths(transitive)); - } - return ret; -} diff --git a/src/dds/usage_reqs.hpp b/src/dds/usage_reqs.hpp deleted file mode 100644 index 6b1ffb14..00000000 --- a/src/dds/usage_reqs.hpp +++ /dev/null @@ -1,51 +0,0 @@ -#pragma once - -#include -#include -#include - -#include - -#include -#include - -namespace dds { - -class shared_compile_file_rules; - -class usage_requirement_map { - - using library_key = lm::usage; - - struct library_key_compare { - bool operator()(const library_key& lhs, const library_key& rhs) const noexcept { - if (lhs.namespace_ < rhs.namespace_) { - return true; - } - if (lhs.namespace_ > rhs.namespace_) { - return false; - } - if (lhs.name < rhs.name) { - return true; - } - return false; - } - }; - - std::map _reqs; - -public: - const lm::library* get(const lm::usage& key) const noexcept; - const lm::library* get(std::string ns, std::string name) const noexcept { - return get({ns, name}); - } - lm::library& add(std::string ns, std::string name); - void add(std::string ns, std::string name, lm::library lib) { add(ns, name) = lib; } - - std::vector link_paths(const lm::usage&) const; - std::vector include_paths(const lm::usage& req) const; - - static usage_requirement_map from_lm_index(const lm::index&) noexcept; -}; - -} // namespace dds diff --git a/src/dds/util/fs.cpp b/src/dds/util/fs.cpp deleted file mode 100644 index 9dcc1d88..00000000 --- a/src/dds/util/fs.cpp +++ /dev/null @@ -1,103 +0,0 @@ -#include "./fs.hpp" - -#include -#include - -#include - -#include - -using namespace dds; - -std::fstream dds::open(const fs::path& filepath, std::ios::openmode mode, std::error_code& ec) { - std::fstream ret{filepath, mode}; - if (!ret) { - ec = std::error_code(errno, std::system_category()); - } - return ret; -} - -std::string dds::slurp_file(const fs::path& path, std::error_code& ec) { - auto file = dds::open(path, std::ios::in, ec); - if (ec) { - return std::string{}; - } - - std::ostringstream out; - out << file.rdbuf(); - return std::move(out).str(); -} - -void dds::safe_rename(path_ref source, path_ref dest) { - std::error_code ec; - fs::rename(source, dest, ec); - if (!ec) { - return; - } - - if (ec != std::errc::cross_device_link && ec != std::errc::permission_denied) { - throw std::system_error(ec, - fmt::format("Failed to move item [{}] to [{}]", - source.string(), - dest.string())); - } - - auto tmp = dest; - tmp.replace_filename(tmp.filename().string() + ".tmp-dds-mv"); - try { - fs::remove_all(tmp); - fs::copy(source, tmp, fs::copy_options::recursive); - } catch (...) { - fs::remove_all(tmp, ec); - throw; - } - fs::rename(tmp, dest); - fs::remove_all(source); -} - -result dds::copy_file(path_ref source, path_ref dest, fs::copy_options opts) noexcept { - std::error_code ec; - fs::copy_file(source, dest, opts, ec); - if (ec) { - return new_error(DDS_E_ARG(e_copy_file{source, dest}), ec); - } - return {}; -} - -result dds::remove_file(path_ref file) noexcept { - std::error_code ec; - fs::remove(file, ec); - if (ec) { - return new_error(DDS_E_ARG(e_remove_file{file}), ec); - } - return {}; -} - -result dds::create_symlink(path_ref target, path_ref symlink) noexcept { - std::error_code ec; - if (fs::is_directory(target)) { - fs::create_directory_symlink(target, symlink, ec); - } else { - fs::create_symlink(target, symlink, ec); - } - if (ec) { - return new_error(DDS_E_ARG(e_symlink{symlink, target}), ec); - } - return {}; -} - -result dds::write_file(path_ref dest, std::string_view content) noexcept { - std::error_code ec; - auto outfile = dds::open(dest, std::ios::binary | std::ios::out, ec); - if (ec) { - return new_error(DDS_E_ARG(e_write_file_path{dest}), ec); - } - errno = 0; - outfile.write(content.data(), content.size()); - auto e = errno; - if (!outfile) { - return new_error(std::error_code(e, std::system_category()), - DDS_E_ARG(e_write_file_path{dest})); - } - return {}; -} \ No newline at end of file diff --git a/src/dds/util/fs.hpp b/src/dds/util/fs.hpp deleted file mode 100644 index edf5c92e..00000000 --- a/src/dds/util/fs.hpp +++ /dev/null @@ -1,68 +0,0 @@ -#pragma once - -#include - -#include -#include -#include -#include - -namespace dds { - -inline namespace file_utils { - -namespace fs = std::filesystem; - -using path_ref = const fs::path&; - -std::fstream open(const fs::path& filepath, std::ios::openmode mode, std::error_code& ec); -std::string slurp_file(const fs::path& path, std::error_code& ec); - -struct e_write_file_path { - fs::path value; -}; -[[nodiscard]] result write_file(const fs::path& path, std::string_view content) noexcept; - -inline std::fstream open(const fs::path& filepath, std::ios::openmode mode) { - std::error_code ec; - auto ret = dds::open(filepath, mode, ec); - if (ec) { - throw std::system_error{ec, "Error opening file: " + filepath.string()}; - } - return ret; -} - -inline std::string slurp_file(const fs::path& path) { - std::error_code ec; - auto contents = dds::slurp_file(path, ec); - if (ec) { - throw std::system_error{ec, "Reading file: " + path.string()}; - } - return contents; -} - -void safe_rename(path_ref source, path_ref dest); - -struct e_copy_file { - fs::path source; - fs::path dest; -}; - -struct e_remove_file { - fs::path value; -}; - -struct e_symlink { - fs::path symlink; - fs::path target; -}; - -[[nodiscard]] result - copy_file(path_ref source, path_ref dest, fs::copy_options opts = {}) noexcept; -[[nodiscard]] result remove_file(path_ref file) noexcept; - -[[nodiscard]] result create_symlink(path_ref target, path_ref symlink) noexcept; - -} // namespace file_utils - -} // namespace dds \ No newline at end of file diff --git a/src/dds/util/output.nix.cpp b/src/dds/util/output.nix.cpp deleted file mode 100644 index fa322df2..00000000 --- a/src/dds/util/output.nix.cpp +++ /dev/null @@ -1,15 +0,0 @@ -#if !_WIN32 - -#include - -#include - -using namespace dds; - -void dds::enable_ansi_console() noexcept { - // unix consoles generally already support ANSI control chars by default -} - -bool dds::stdout_is_a_tty() noexcept { return ::isatty(STDOUT_FILENO) != 0; } - -#endif diff --git a/src/dds/util/parallel.cpp b/src/dds/util/parallel.cpp deleted file mode 100644 index d0ad7992..00000000 --- a/src/dds/util/parallel.cpp +++ /dev/null @@ -1,17 +0,0 @@ -#include "./parallel.hpp" - -#include - -#include - -using namespace dds; - -void dds::log_exception(std::exception_ptr eptr) noexcept { - try { - std::rethrow_exception(eptr); - } catch (const dds::user_cancelled&) { - // Don't log this one. The user knows what they did - } catch (const std::exception& e) { - dds_log(error, "{}", e.what()); - } -} diff --git a/src/dds/util/paths.hpp b/src/dds/util/paths.hpp deleted file mode 100644 index 886ab474..00000000 --- a/src/dds/util/paths.hpp +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include - -namespace dds { - -fs::path user_home_dir(); -fs::path user_data_dir(); -fs::path user_cache_dir(); -fs::path user_config_dir(); - -inline fs::path dds_data_dir() { return user_data_dir() / "dds"; } -inline fs::path dds_cache_dir() { return user_cache_dir() / "dds-cache"; } -inline fs::path dds_config_dir() { return user_config_dir() / "dds"; } - -} // namespace dds \ No newline at end of file diff --git a/src/dds/util/paths.macos.cpp b/src/dds/util/paths.macos.cpp deleted file mode 100644 index 11daacdb..00000000 --- a/src/dds/util/paths.macos.cpp +++ /dev/null @@ -1,26 +0,0 @@ -#ifdef __APPLE__ - -#include "./paths.hpp" - -#include -#include - -#include - -using namespace dds; - -fs::path dds::user_home_dir() { - static auto ret = []() -> fs::path { - return fs::absolute(dds::getenv("HOME", [] { - dds_log(error, "No HOME environment variable set!"); - return "/"; - })); - }(); - return ret; -} - -fs::path dds::user_data_dir() { return user_home_dir() / "Library/Application Support"; } -fs::path dds::user_cache_dir() { return user_home_dir() / "Library/Caches"; } -fs::path dds::user_config_dir() { return user_home_dir() / "Preferences"; } - -#endif diff --git a/src/dds/util/result.cpp b/src/dds/util/result.cpp deleted file mode 100644 index 022bee2b..00000000 --- a/src/dds/util/result.cpp +++ /dev/null @@ -1,32 +0,0 @@ -#include "./result.hpp" - -#include -#include - -#include -#include - -#include - -void dds::capture_exception() { - try { - throw; - } catch (const neo::sqlite3::sqlite3_error& e) { - current_error().load(e_sqlite3_error_exc{std::string(e.what()), e.code()}, - e.code(), - neo::sqlite3::errc{e.code().value()}); - } catch (const std::system_error& e) { - current_error().load(e_system_error_exc{std::string(e.what()), e.code()}, e.code()); - } - // Re-throw as a bare exception. - throw std::exception(); -} - -void dds::write_error_marker(std::string_view error) noexcept { - dds_log(trace, "[error marker {}]", error); - auto efile_path = dds::getenv("DDS_WRITE_ERROR_MARKER"); - if (efile_path) { - std::ofstream outfile{*efile_path, std::ios::binary}; - fmt::print(outfile, "{}", error); - } -} diff --git a/src/dds/util/result.hpp b/src/dds/util/result.hpp deleted file mode 100644 index 165d30e9..00000000 --- a/src/dds/util/result.hpp +++ /dev/null @@ -1,69 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -namespace dds { - -using boost::leaf::current_error; -using boost::leaf::error_id; -using boost::leaf::new_error; -using boost::leaf::result; - -template U> -constexpr T value_or(const result& res, U&& arg) { - return res ? res.value() : static_cast(arg); -} - -template -using matchv = boost::leaf::match; - -/** - * @brief Error object representing a captured system_error exception - */ -struct e_system_error_exc { - std::string message; - std::error_code code; -}; - -/** - * @brief Error object representing a captured neo::sqlite3::sqlite3_error - */ -struct e_sqlite3_error_exc { - std::string message; - std::error_code code; -}; - -struct e_url_string { - std::string value; -}; - -struct e_human_message { - std::string value; -}; - -struct e_missing_file { - std::filesystem::path path; -}; - -/** - * @brief Capture currently in-flight special exceptions as new error object. Works around a bug in - * Boost.LEAF when catching std::system error. - */ -[[noreturn]] void capture_exception(); - -void write_error_marker(std::string_view error) noexcept; - -} // namespace dds diff --git a/src/debate/argument.cpp b/src/debate/argument.cpp index 810ad3ba..e7c438ca 100644 --- a/src/debate/argument.cpp +++ b/src/debate/argument.cpp @@ -1,5 +1,8 @@ #include "./argument.hpp" +#include +#include + #include using namespace debate; @@ -44,25 +47,46 @@ std::string argument::preferred_spelling() const noexcept { std::string argument::syntax_string() const noexcept { std::string ret; - if (!required) { - ret.push_back('['); - } + auto pref_spell = preferred_spelling(); + auto real_valname = !valname.empty() + ? valname + : (long_spellings.empty() ? "" : ("<" + long_spellings[0] + ">")); if (is_positional()) { - ret.append(preferred_spelling()); - } else { - ret.append(preferred_spelling()); - if (nargs != 0) { - auto real_valname = !valname.empty() - ? valname - : (long_spellings.empty() ? "" : ("<" + long_spellings[0] + ">")); - ret.append(fmt::format(" {}", valname.empty() ? "" : valname)); + if (required) { + if (can_repeat) { + ret.append(neo::ufmt("{} [{} [...]]", real_valname, real_valname)); + } else { + ret.append(real_valname); + } + } else { + if (can_repeat) { + ret.append(neo::ufmt("[{} [{} [...]]]", real_valname, real_valname)); + } else { + ret.append(neo::ufmt("[{}]", real_valname)); + } } - } - if (can_repeat) { - ret.append(" ..."); - } - if (!required) { - ret.push_back(']'); + } else if (nargs != 0) { + char sep_char = pref_spell.starts_with("--") ? '=' : ' '; + if (required) { + ret.append(neo::ufmt("{}{}{}", pref_spell, sep_char, real_valname)); + if (can_repeat) { + ret.append(neo::ufmt(" [{}{}{} [...]]", pref_spell, sep_char, real_valname)); + } + } else { + if (can_repeat) { + ret.append(neo::ufmt("[{}{}{} [{}{}{} [...]]]", + pref_spell, + sep_char, + real_valname, + pref_spell, + sep_char, + real_valname)); + } else { + ret.append(neo::ufmt("[{}{}{}]", pref_spell, sep_char, real_valname)); + } + } + } else { + ret.append(neo::ufmt("[{}]", pref_spell)); } return ret; } @@ -99,3 +123,38 @@ std::string argument::help_string() const noexcept { return ret; } + +bool debate::parse_bool_string(std::string_view sv) { + if (sv + == neo::oper::any_of("y", // + "Y", + "yes", + "YES", + "Yes", + "true", + "TRUE", + "True", + "on", + "ON", + "On", + "1")) { + return true; + } else if (sv + == neo::oper::any_of("n", + "N", + "no", + "NO", + "No", + "false", + "FALSE", + "False", + "off", + "OFF", + "Off", + "0")) { + return false; + } else { + BOOST_LEAF_THROW_EXCEPTION(invalid_arguments("Invalid string for boolean/toggle option"), + e_invalid_arg_value{std::string(sv)}); + } +} diff --git a/src/debate/argument.hpp b/src/debate/argument.hpp index d962bd9c..f72c54c5 100644 --- a/src/debate/argument.hpp +++ b/src/debate/argument.hpp @@ -3,7 +3,11 @@ #include "./error.hpp" #include -#include +#include + +#if __has_include() +#include "./enum.hpp" +#endif #include #include @@ -16,6 +20,8 @@ namespace debate { template constexpr auto make_enum_putter(E& dest) noexcept; +bool parse_bool_string(std::string_view sv); + template class argument_value_putter { T& _dest; @@ -27,6 +33,17 @@ class argument_value_putter { void operator()(std::string_view value, std::string_view) { _dest = T(value); } }; +template +class argument_value_putter> { + std::optional& _dest; + +public: + explicit argument_value_putter(std::optional& opt) + : _dest(opt) {} + + void operator()(std::string_view value, std::string_view) { _dest = static_cast(value); } +}; + template class integer_putter { Int& _dest; @@ -49,7 +66,7 @@ class integer_putter { template constexpr auto make_argument_putter(T& dest) { if constexpr (std::is_enum_v) { - return make_enum_putter(dest); /// !! README: Include to use enums here + return make_enum_putter(dest); } else if constexpr (std::is_integral_v) { return integer_putter(dest); } else { @@ -64,6 +81,13 @@ constexpr inline auto store_value = [](auto& dest, auto val) { constexpr inline auto store_true = [](auto& dest) { return store_value(dest, true); }; constexpr inline auto store_false = [](auto& dest) { return store_value(dest, false); }; +constexpr inline auto parse_bool_into = [](auto& dest) { + return [&dest](std::string_view given, std::string_view spell) -> void { + auto _ = boost::leaf::on_error(e_arg_spelling{std::string(spell)}); + dest = parse_bool_string(given); + }; +}; + constexpr inline auto put_into = [](auto& dest) { return make_argument_putter(dest); }; constexpr inline auto push_back_onto = [](auto& dest) { @@ -83,7 +107,7 @@ struct argument { std::function action; - // This member variable makes this strunct noncopyable, and has no other purpose + // This member variable makes this struct noncopyable, and has no other purpose std::unique_ptr _make_noncopyable{}; std::string_view try_match_short(std::string_view arg) const noexcept; std::string_view try_match_long(std::string_view arg) const noexcept; diff --git a/src/debate/argument_parser.cpp b/src/debate/argument_parser.cpp index c51e5bfb..5217b0f2 100644 --- a/src/debate/argument_parser.cpp +++ b/src/debate/argument_parser.cpp @@ -1,7 +1,7 @@ #include "./argument_parser.hpp" -/// XXX: Refactor this after pulling debate:: out of dds -#include +/// XXX: Refactor this after pulling debate:: out of bpt +#include #include #include @@ -36,7 +36,7 @@ struct parse_engine { void see(const argument& arg) { auto did_insert = seen.insert(&arg).second; if (!did_insert && !arg.can_repeat) { - BOOST_LEAF_THROW_EXCEPTION(invalid_repitition("Invalid repitition")); + BOOST_LEAF_THROW_EXCEPTION(invalid_repetition("Invalid repetition")); } } @@ -51,7 +51,7 @@ struct parse_engine { std::optional find_nearest_arg_spelling(std::string_view given) const noexcept { std::vector candidates; - // Only match arguments of the corrent type + // Only match arguments of the correct type auto parser = bottom_parser; while (parser) { for (auto& arg : parser->arguments()) { @@ -70,7 +70,7 @@ struct parse_engine { candidates.push_back(p.name); } } - return dds::did_you_mean(given, candidates); + return bpt::did_you_mean(given, candidates); } void parse_another() { @@ -240,6 +240,10 @@ struct parse_engine { if (!arg.nargs) { // Just a switch. Consume a single character arg.action("", spelling); + if (tail.empty()) { + // A lone switch + shift(); + } return tail; } else if (arg.nargs == 1) { // Want one value diff --git a/src/debate/argument_parser.hpp b/src/debate/argument_parser.hpp index 33055290..96739be0 100644 --- a/src/debate/argument_parser.hpp +++ b/src/debate/argument_parser.hpp @@ -2,7 +2,6 @@ #include "./argument.hpp" -#include #include #include @@ -36,7 +35,7 @@ struct parser_state_impl : parser_state { bool at_end() const noexcept override { return arg_it == arg_stop; } std::string_view current_arg() const noexcept override { - neo_assert(invariant, !at_end(), "Get argument past the final argumetn?"); + neo_assert(invariant, !at_end(), "Get argument past the final argument?"); return *arg_it; } void shift() noexcept override { @@ -70,7 +69,7 @@ class argument_parser { std::optional _subparsers; std::string _name; std::string _description; - // The parent of this argumetn parser, if it was attached using a subparser_group + // The parent of this argument parser, if it was attached using a subparser_group neo::opt_ref _parent; using strv = std::string_view; diff --git a/src/debate/argument_parser.test.cpp b/src/debate/argument_parser.test.cpp index 5624878a..1c68a93b 100644 --- a/src/debate/argument_parser.test.cpp +++ b/src/debate/argument_parser.test.cpp @@ -2,11 +2,12 @@ #include "./enum.hpp" +#include #include TEST_CASE("Create an argument parser") { enum log_level { - _invalid, + invalid_, info, warning, error, @@ -28,20 +29,20 @@ TEST_CASE("Create an argument parser") { .valname = "", .action = debate::put_into(file), }); - parser.parse_argv({"--log-level=info"}); + REQUIRES_LEAF_NOFAIL(parser.parse_argv({"--log-level=info"})); CHECK(level == log_level::info); - parser.parse_argv({"--log-level=warning"}); + REQUIRES_LEAF_NOFAIL(parser.parse_argv({"--log-level=warning"})); CHECK(level == log_level::warning); - parser.parse_argv({"--log-level", "info"}); + REQUIRES_LEAF_NOFAIL(parser.parse_argv({"--log-level", "info"})); CHECK(level == log_level::info); - parser.parse_argv({"-lerror"}); + REQUIRES_LEAF_NOFAIL(parser.parse_argv({"-lerror"})); CHECK(level == log_level::error); CHECK_THROWS_AS(parser.parse_argv({"-lerror", "--log-level=info"}), std::runtime_error); - parser.parse_argv({"-l", "info"}); + REQUIRES_LEAF_NOFAIL(parser.parse_argv({"-l", "info"})); CHECK(level == log_level::info); - parser.parse_argv({"-lwarning", "my-file.txt"}); + REQUIRES_LEAF_NOFAIL(parser.parse_argv({"-lwarning", "my-file.txt"})); CHECK(level == log_level::warning); CHECK(file == "my-file.txt"); } diff --git a/src/debate/enum.hpp b/src/debate/enum.hpp index b0b95c21..7f8ec6bc 100644 --- a/src/debate/enum.hpp +++ b/src/debate/enum.hpp @@ -8,6 +8,7 @@ #include #include +#include #include namespace debate { @@ -16,29 +17,32 @@ template class enum_putter { E* _dest; + static std::string _kebab_name(std::string val_ident) { + std::ranges::replace(val_ident, '_', '-'); + auto trim_pos = val_ident.find_last_not_of("-"); + if (trim_pos != std::string::npos) { + val_ident.erase(trim_pos + 1); + } + return val_ident; + } + public: constexpr explicit enum_putter(E& e) : _dest(&e) {} void operator()(std::string_view given, std::string_view full_arg) const { - std::optional normalized_str; - std::string_view normalized_view = given; - if (given.find('-') != given.npos) { - // We should normalize it - normalized_str.emplace(given); - for (char& c : *normalized_str) { - c = c == '-' ? '_' : c; - } - normalized_view = *normalized_str; - } - auto val = magic_enum::enum_cast(normalized_view); - if (!val) { + auto entries = magic_enum::enum_entries(); + auto matching = std::ranges::find(entries, given, [](auto&& p) { + return _kebab_name(std::string(p.second)); + }); + if (matching == std::ranges::end(entries)) { throw boost::leaf:: exception(invalid_arguments("Invalid argument value given for enum-bound argument"), e_invalid_arg_value{std::string(given)}, e_arg_spelling{std::string(full_arg)}); } - *_dest = *val; + + *_dest = matching->first; } }; diff --git a/src/debate/error.hpp b/src/debate/error.hpp index a8c3b21b..061425d9 100644 --- a/src/debate/error.hpp +++ b/src/debate/error.hpp @@ -24,32 +24,32 @@ struct missing_required : invalid_arguments { using invalid_arguments::invalid_arguments; }; -struct invalid_repitition : invalid_arguments { +struct invalid_repetition : invalid_arguments { using invalid_arguments::invalid_arguments; }; struct e_argument { - const debate::argument& argument; + const debate::argument& value; }; struct e_argument_parser { - const debate::argument_parser& parser; + const debate::argument_parser& value; }; struct e_invalid_arg_value { - std::string given; + std::string value; }; struct e_wrong_val_num { - int n_given; + int value; }; struct e_arg_spelling { - std::string spelling; + std::string value; }; struct e_did_you_mean { - std::string candidate; + std::string value; }; } // namespace debate diff --git a/src/fansi/style.hpp b/src/fansi/style.hpp index a4476a68..84a1f91e 100644 --- a/src/fansi/style.hpp +++ b/src/fansi/style.hpp @@ -11,7 +11,7 @@ enum class std_color { green = 2, yellow = 3, blue = 4, - magent = 5, + magenta = 5, cyan = 6, white = 7, normal = 9, diff --git a/src/fansi/styled.test.cpp b/src/fansi/styled.test.cpp index 2ab993b3..0c725e16 100644 --- a/src/fansi/styled.test.cpp +++ b/src/fansi/styled.test.cpp @@ -14,7 +14,7 @@ TEST_CASE("Stylize some text") { test = render("foo `.eggs"); CHECK(test == "foo .eggs"); - test = render("foo `.bar[`]"); + test = render("foo `.bar[]"); CHECK(test == "foo .bar[]"); test = render("foo .bold[bar] baz"); diff --git a/src/libman/index.cpp b/src/libman/index.cpp index 60aaea41..73671596 100644 --- a/src/libman/index.cpp +++ b/src/libman/index.cpp @@ -14,8 +14,7 @@ lm::index index::from_file(path_ref fpath) { // fmt::format("Libman file has missing/incorrect 'Type' ({})", fpath.string())); // } - index ret; - + index ret; std::optional type; std::vector package_lines; @@ -26,7 +25,7 @@ lm::index index::from_file(path_ref fpath) { read_accumulate("Package", package_lines)); for (const auto& pkg_line : package_lines) { - auto items = dds::split(pkg_line, ";"); + auto items = bpt::split(pkg_line, ";"); std::transform(items.begin(), items.end(), items.begin(), [](auto s) { return std::string(trim_view(s)); }); diff --git a/src/libman/library.cpp b/src/libman/library.cpp index 101f1ccb..e61ffe70 100644 --- a/src/libman/library.cpp +++ b/src/libman/library.cpp @@ -1,28 +1,40 @@ #include "./library.hpp" +#include +#include +#include + #include #include using namespace lm; -library library::from_file(path_ref fpath) { +bpt::result library::from_file(path_ref fpath) { auto pairs = parse_file(fpath); library ret; std::string _type_; - read(fmt::format("Reading library manifest file '{}'", fpath.string()), - pairs, - read_required("Type", _type_), - read_check_eq("Type", "Library"), - read_required("Name", ret.name), - read_opt("Path", ret.linkable_path), - read_accumulate("Include-Path", ret.include_paths), - read_accumulate("Preprocessor-Define", ret.preproc_defs), - read_accumulate("Uses", ret.uses, &split_usage_string), - read_accumulate("Links", ret.links, &split_usage_string), - read_accumulate("Special-Uses", ret.special_uses)); + try { + read(fmt::format("Reading library manifest file '{}'", fpath.string()), + pairs, + read_required("Type", _type_), + read_check_eq("Type", "Library"), + read_required("Name", ret.name), + read_opt("Path", ret.linkable_path), + read_accumulate("Include-Path", ret.include_paths), + read_accumulate("Preprocessor-Define", ret.preproc_defs), + read_accumulate("Uses", + ret.uses, + BPT_TL(bpt::decay_copy(split_usage_string(_1).value()))), + read_accumulate("Links", + ret.links, + BPT_TL(bpt::decay_copy(split_usage_string(_1).value()))), + read_accumulate("Special-Uses", ret.special_uses)); + } catch (const boost::leaf::bad_result& err) { + return err.load(); + } auto make_absolute = [&](path_ref p) { return fpath.parent_path() / p; }; std::transform(ret.include_paths.begin(), @@ -36,13 +48,3 @@ library library::from_file(path_ref fpath) { return ret; } - -usage lm::split_usage_string(std::string_view str) { - auto sl_pos = str.find('/'); - if (sl_pos == str.npos) { - throw std::runtime_error("Invalid Uses/Links specifier: " + std::string(str)); - } - auto ns = str.substr(0, sl_pos); - auto name = str.substr(sl_pos + 1); - return usage{std::string(ns), std::string(name)}; -} \ No newline at end of file diff --git a/src/libman/library.hpp b/src/libman/library.hpp index dd713df8..00bb0f22 100644 --- a/src/libman/library.hpp +++ b/src/libman/library.hpp @@ -1,31 +1,40 @@ #pragma once +#include "./usage.hpp" +#include #include #include #include #include -namespace lm { - -struct usage { - std::string namespace_; - std::string name; -}; +#include -usage split_usage_string(std::string_view); +namespace lm { class library { public: std::string name; - std::optional linkable_path; - std::vector include_paths; - std::vector preproc_defs; - std::vector uses; - std::vector links; - std::vector special_uses; - - static library from_file(path_ref); + std::optional linkable_path{}; + std::vector include_paths{}; + std::vector preproc_defs{}; + std::vector uses{}; + std::vector links{}; + std::vector special_uses{}; + + static bpt::result from_file(path_ref); }; -} // namespace lm \ No newline at end of file +} // namespace lm + +namespace fmt { +template <> +struct formatter { + constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } + + template + auto format(const lm::usage& usage, FormatContext& ctx) { + return format_to(ctx.out(), "'{}/{}'", usage.namespace_, usage.name); + } +}; +} // namespace fmt \ No newline at end of file diff --git a/src/libman/package.cpp b/src/libman/package.cpp index de3c669c..6501548e 100644 --- a/src/libman/package.cpp +++ b/src/libman/package.cpp @@ -2,6 +2,7 @@ #include +#include #include using namespace lm; @@ -22,7 +23,7 @@ package package::from_file(path_ref fpath) { read_accumulate("Library", libraries)); for (path_ref lib_path : libraries) { - ret.libraries.push_back(library::from_file(fpath.parent_path() / lib_path)); + ret.libraries.push_back(library::from_file(fpath.parent_path() / lib_path).value()); } return ret; diff --git a/src/libman/parse.cpp b/src/libman/parse.cpp index 80ec27d8..3ab38628 100644 --- a/src/libman/parse.cpp +++ b/src/libman/parse.cpp @@ -1,5 +1,6 @@ #include "./parse.hpp" +#include #include #include @@ -74,10 +75,10 @@ pair_list lm::parse_string(std::string_view s) { return pair_list(std::move(pairs)); } -lm::pair_list lm::parse_file(fs::path fpath) { return parse_string(dds::slurp_file(fpath)); } +lm::pair_list lm::parse_file(fs::path fpath) { return parse_string(bpt::read_file(fpath)); } void lm::write_pairs(fs::path fpath, const std::vector& pairs) { - auto fstream = dds::open(fpath, std::ios::out | std::ios::binary); + auto fstream = bpt::open_file(fpath, std::ios::out | std::ios::binary); for (auto& pair : pairs) { fstream << pair.key << ": " << pair.value << '\n'; } diff --git a/src/libman/parse.hpp b/src/libman/parse.hpp index 37ba58e5..0fe0c7f9 100644 --- a/src/libman/parse.hpp +++ b/src/libman/parse.hpp @@ -282,7 +282,7 @@ class read_accumulate { , _items(c) , _tr(unchanged()) {} - int operator()(std::string_view, std::string_view key, std::string_view value) const { + int operator()(std::string_view, std::string_view key, std::string_view value) { if (key == _key) { _items.emplace_back(_tr(value)); return 1; diff --git a/src/libman/usage.cpp b/src/libman/usage.cpp new file mode 100644 index 00000000..b7f66f34 --- /dev/null +++ b/src/libman/usage.cpp @@ -0,0 +1,21 @@ +#include "./usage.hpp" + +#include + +#include + +#include + +using namespace lm; + +bpt::result lm::split_usage_string(std::string_view str) { + auto sl_pos = str.find('/'); + if (sl_pos == str.npos) { + return boost::leaf::new_error(e_invalid_usage_string{std::string(str)}); + } + auto ns = str.substr(0, sl_pos); + auto name = str.substr(sl_pos + 1); + return usage{std::string(ns), std::string(name)}; +} + +void usage::write_to(std::ostream& out) const noexcept { out << namespace_ << "/" << name; } diff --git a/src/libman/usage.hpp b/src/libman/usage.hpp new file mode 100644 index 00000000..89a922cb --- /dev/null +++ b/src/libman/usage.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include + +#include +#include + +namespace lm { + +struct e_invalid_usage_string { + std::string value; +}; + +struct usage { + std::string namespace_; + std::string name; + + auto operator<=>(const usage&) const noexcept = default; + + void write_to(std::ostream&) const noexcept; + + friend std::ostream& operator<<(std::ostream& out, const usage& self) noexcept { + self.write_to(out); + return out; + } +}; + +bpt::result split_usage_string(std::string_view); + +} // namespace lm \ No newline at end of file diff --git a/src/libman/util.hpp b/src/libman/util.hpp index 32a1743a..73bbffcb 100644 --- a/src/libman/util.hpp +++ b/src/libman/util.hpp @@ -1,11 +1,12 @@ #pragma once -#include -#include +#include +#include namespace lm { -using namespace dds::file_utils; -using namespace dds::string_utils; +namespace fs = bpt::fs; +using bpt::path_ref; +using namespace bpt::string_utils; } // namespace lm \ No newline at end of file diff --git a/tests/colliding_libs_objects/proj-exec/bpt.yaml b/tests/colliding_libs_objects/proj-exec/bpt.yaml new file mode 100644 index 00000000..58b7e2e3 --- /dev/null +++ b/tests/colliding_libs_objects/proj-exec/bpt.yaml @@ -0,0 +1,9 @@ +{ + "name": "testing", + "version": "1.2.3", + "libraries": + [ + { "path": "libs/hello", "name": "hello" }, + { "path": "libs/hello2", "name": "hello2" }, + ], +} diff --git a/tests/colliding_libs_objects/proj-exec/libs/hello/src/hello.test.cpp b/tests/colliding_libs_objects/proj-exec/libs/hello/src/hello.test.cpp new file mode 100644 index 00000000..074a7d76 --- /dev/null +++ b/tests/colliding_libs_objects/proj-exec/libs/hello/src/hello.test.cpp @@ -0,0 +1,3 @@ +int main() { + return 0; // Success! +} \ No newline at end of file diff --git a/tests/colliding_libs_objects/proj-exec/libs/hello2/src/hello.test.cpp b/tests/colliding_libs_objects/proj-exec/libs/hello2/src/hello.test.cpp new file mode 100644 index 00000000..9dc7a5ef --- /dev/null +++ b/tests/colliding_libs_objects/proj-exec/libs/hello2/src/hello.test.cpp @@ -0,0 +1,3 @@ +int main() { + return 1; // Failure +} \ No newline at end of file diff --git a/tests/colliding_libs_objects/proj/libs/hello/src/hello.cpp b/tests/colliding_libs_objects/proj/libs/hello/src/hello.cpp new file mode 100644 index 00000000..36a2c055 --- /dev/null +++ b/tests/colliding_libs_objects/proj/libs/hello/src/hello.cpp @@ -0,0 +1,5 @@ +#include + +namespace hello { +std::string message() { return "Hello 1"; } +} // namespace hello \ No newline at end of file diff --git a/tests/colliding_libs_objects/proj/libs/hello/src/hello.test.cpp b/tests/colliding_libs_objects/proj/libs/hello/src/hello.test.cpp new file mode 100644 index 00000000..1b0814ee --- /dev/null +++ b/tests/colliding_libs_objects/proj/libs/hello/src/hello.test.cpp @@ -0,0 +1,13 @@ +#include +#include + +namespace hello { +std::string message(); +} + +int main() { + auto msg = hello::message(); + std::cout << msg << '\n'; + if (msg != "Hello 1") + return 1; +} \ No newline at end of file diff --git a/tests/colliding_libs_objects/proj/libs/hello2/src/hello.cpp b/tests/colliding_libs_objects/proj/libs/hello2/src/hello.cpp new file mode 100644 index 00000000..367191aa --- /dev/null +++ b/tests/colliding_libs_objects/proj/libs/hello2/src/hello.cpp @@ -0,0 +1,5 @@ +#include + +namespace hello { +std::string message() { return "Hello 2"; } +} // namespace hello \ No newline at end of file diff --git a/tests/colliding_libs_objects/proj/libs/hello2/src/hello2.test.cpp b/tests/colliding_libs_objects/proj/libs/hello2/src/hello2.test.cpp new file mode 100644 index 00000000..0c808bba --- /dev/null +++ b/tests/colliding_libs_objects/proj/libs/hello2/src/hello2.test.cpp @@ -0,0 +1,13 @@ +#include +#include + +namespace hello { +std::string message(); +} + +int main() { + auto msg = hello::message(); + std::cout << msg << '\n'; + if (msg != "Hello 2") + return 1; +} \ No newline at end of file diff --git a/tests/colliding_libs_objects/test_colliding_libs_objects.py b/tests/colliding_libs_objects/test_colliding_libs_objects.py new file mode 100644 index 00000000..3ae6de6e --- /dev/null +++ b/tests/colliding_libs_objects/test_colliding_libs_objects.py @@ -0,0 +1,14 @@ +from bpt_ci.testing import ProjectOpener +from bpt_ci.testing.error import expect_error_marker + + +def test_compile_libs_with_colliding_object_files(project_opener: ProjectOpener) -> None: + proj = project_opener.open('proj') + proj.build() + + +def test_compile_libs_with_colliding_test_executables(project_opener: ProjectOpener) -> None: + proj = project_opener.open('proj-exec') + with expect_error_marker('build-failed-test-failed'): + # Use only a single process to make the failure consistent + proj.build(jobs=1) diff --git a/tests/config_template/copy_only/src/info.config.hpp b/tests/config_template/copy_only/src/info.config.hpp deleted file mode 100644 index 1190151d..00000000 --- a/tests/config_template/copy_only/src/info.config.hpp +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include - -int config_file_value = 42; diff --git a/tests/config_template/copy_only/src/info.test.cpp b/tests/config_template/copy_only/src/info.test.cpp deleted file mode 100644 index caf4ae8b..00000000 --- a/tests/config_template/copy_only/src/info.test.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include - -#include - -int main() { assert(config_file_value == 42); } diff --git a/tests/config_template/simple/library.jsonc b/tests/config_template/simple/library.jsonc deleted file mode 100644 index 6c300d07..00000000 --- a/tests/config_template/simple/library.jsonc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "test-library" -} \ No newline at end of file diff --git a/tests/config_template/simple/package.jsonc b/tests/config_template/simple/package.jsonc deleted file mode 100644 index 6b63f8d2..00000000 --- a/tests/config_template/simple/package.jsonc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "test-simple", - "version": "1.2.3-gamma", - "namespace": "test" -} \ No newline at end of file diff --git a/tests/config_template/simple/src/simple/config.config.hpp b/tests/config_template/simple/src/simple/config.config.hpp deleted file mode 100644 index 6efa5bde..00000000 --- a/tests/config_template/simple/src/simple/config.config.hpp +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include - -std::string_view lib_name = __dds(lib.name); diff --git a/tests/config_template/simple/src/simple/simple.test.cpp b/tests/config_template/simple/src/simple/simple.test.cpp deleted file mode 100644 index 3c099cab..00000000 --- a/tests/config_template/simple/src/simple/simple.test.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include - -#include - -int main() { assert(lib_name == "test-library"); } \ No newline at end of file diff --git a/tests/config_template/test_config_template.py b/tests/config_template/test_config_template.py deleted file mode 100644 index c6f14723..00000000 --- a/tests/config_template/test_config_template.py +++ /dev/null @@ -1,25 +0,0 @@ -from time import sleep - -from dds_ci.testing import ProjectOpener - - -def test_config_template(project_opener: ProjectOpener) -> None: - proj = project_opener.open('copy_only') - generated_fpath = proj.build_root / '__dds/gen/info.hpp' - assert not generated_fpath.is_file() - proj.build() - assert generated_fpath.is_file() - - # Check that re-running the build will not update the generated file (the - # file's content has not changed. Re-generating it would invalidate the - # cache and force a false-rebuild.) - start_time = generated_fpath.stat().st_mtime - sleep(0.1) # Wait just long enough to register a new stamp time - proj.build() - new_time = generated_fpath.stat().st_mtime - assert new_time == start_time - - -def test_simple_substitution(project_opener: ProjectOpener) -> None: - simple = project_opener.open('simple') - simple.build() diff --git a/tests/conftest.py b/tests/conftest.py index 8e395997..be670422 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,16 +5,19 @@ from _pytest.config import Config as PyTestConfig # Ensure the fixtures are registered with PyTest: -from dds_ci.testing.fixtures import * # pylint: disable=wildcard-import,unused-wildcard-import -from dds_ci.testing.http import * # pylint: disable=wildcard-import,unused-wildcard-import +from bpt_ci.testing.fs import * # pylint: disable=wildcard-import,unused-wildcard-import +from bpt_ci.testing.fixtures import * # pylint: disable=wildcard-import,unused-wildcard-import +from bpt_ci.testing.http import * # pylint: disable=wildcard-import,unused-wildcard-import +from bpt_ci.testing.repo import * # pylint: disable=wildcard-import,unused-wildcard-import def pytest_addoption(parser: Any) -> None: parser.addoption('--test-deps', action='store_true', default=False, - help='Run the exhaustive and intensive dds-deps tests') - parser.addoption('--dds-exe', help='Path to the dds executable under test', type=Path) + help='Run the exhaustive and intensive bpt-deps tests') + parser.addoption('--bpt-exe', help='Path to the bpt executable under test', type=Path) + parser.addoption('--git-exe', help='Path to the git executable', type=Path) def pytest_configure(config: Any) -> None: diff --git a/tests/gcc-10.tc.jsonc b/tests/gcc-10.tc.jsonc new file mode 100644 index 00000000..1ee2fb85 --- /dev/null +++ b/tests/gcc-10.tc.jsonc @@ -0,0 +1,5 @@ +{ + "compiler_id": 'gnu', + "cxx_version": 'c++20', + "cxx_compiler": 'g++-10', +} \ No newline at end of file diff --git a/tests/gcc-9.tc.jsonc b/tests/gcc-9.tc.jsonc deleted file mode 100644 index a0a03867..00000000 --- a/tests/gcc-9.tc.jsonc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "compiler_id": 'gnu', - "cxx_version": 'c++17', - "cxx_compiler": 'g++-9', -} \ No newline at end of file diff --git a/tests/isolated_headers/bad_proj/include/bad_proj/good.hpp b/tests/isolated_headers/bad_proj/include/bad_proj/good.hpp new file mode 100644 index 00000000..6f70f09b --- /dev/null +++ b/tests/isolated_headers/bad_proj/include/bad_proj/good.hpp @@ -0,0 +1 @@ +#pragma once diff --git a/tests/isolated_headers/bad_proj/src/bad_proj/bad_src.cpp b/tests/isolated_headers/bad_proj/src/bad_proj/bad_src.cpp new file mode 100644 index 00000000..42e4da10 --- /dev/null +++ b/tests/isolated_headers/bad_proj/src/bad_proj/bad_src.cpp @@ -0,0 +1,5 @@ +struct bad_src {}; + +#include "./bad_src.hpp" + +bad_src bad_function() { return bad_src{}; } diff --git a/tests/isolated_headers/bad_proj/src/bad_proj/bad_src.hpp b/tests/isolated_headers/bad_proj/src/bad_proj/bad_src.hpp new file mode 100644 index 00000000..a25b8a9b --- /dev/null +++ b/tests/isolated_headers/bad_proj/src/bad_proj/bad_src.hpp @@ -0,0 +1,3 @@ +#pragma once + +bad_src bad_function(); diff --git a/tests/isolated_headers/bad_proj/src/bad_proj/src_header.hpp b/tests/isolated_headers/bad_proj/src/bad_proj/src_header.hpp new file mode 100644 index 00000000..6f70f09b --- /dev/null +++ b/tests/isolated_headers/bad_proj/src/bad_proj/src_header.hpp @@ -0,0 +1 @@ +#pragma once diff --git a/tests/isolated_headers/bad_proj_include/include/bad_proj/depends_src.hpp b/tests/isolated_headers/bad_proj_include/include/bad_proj/depends_src.hpp new file mode 100644 index 00000000..6fb26fd9 --- /dev/null +++ b/tests/isolated_headers/bad_proj_include/include/bad_proj/depends_src.hpp @@ -0,0 +1,3 @@ +#pragma once + +#include "bad_proj/src_header.hpp" diff --git a/tests/isolated_headers/bad_proj_include/src/bad_proj/src_header.cpp b/tests/isolated_headers/bad_proj_include/src/bad_proj/src_header.cpp new file mode 100644 index 00000000..c05f5988 --- /dev/null +++ b/tests/isolated_headers/bad_proj_include/src/bad_proj/src_header.cpp @@ -0,0 +1 @@ +#include "./src_header.hpp" diff --git a/tests/isolated_headers/bad_proj_include/src/bad_proj/src_header.hpp b/tests/isolated_headers/bad_proj_include/src/bad_proj/src_header.hpp new file mode 100644 index 00000000..6f70f09b --- /dev/null +++ b/tests/isolated_headers/bad_proj_include/src/bad_proj/src_header.hpp @@ -0,0 +1 @@ +#pragma once diff --git a/tests/isolated_headers/good_proj_inl/include/good.hpp b/tests/isolated_headers/good_proj_inl/include/good.hpp new file mode 100644 index 00000000..6b065339 --- /dev/null +++ b/tests/isolated_headers/good_proj_inl/include/good.hpp @@ -0,0 +1,10 @@ +#pragma once + +template +struct SomeType { + int doSomething(); + int doSomething2(); +}; + +#include +#include \ No newline at end of file diff --git a/tests/isolated_headers/good_proj_inl/include/good.inl b/tests/isolated_headers/good_proj_inl/include/good.inl new file mode 100644 index 00000000..b88d2b6d --- /dev/null +++ b/tests/isolated_headers/good_proj_inl/include/good.inl @@ -0,0 +1,2 @@ +template +int SomeType::doSomething() { return sizeof(T); } \ No newline at end of file diff --git a/tests/isolated_headers/good_proj_inl/include/good.ipp b/tests/isolated_headers/good_proj_inl/include/good.ipp new file mode 100644 index 00000000..94914b05 --- /dev/null +++ b/tests/isolated_headers/good_proj_inl/include/good.ipp @@ -0,0 +1,2 @@ +template +int SomeType::doSomething2() { return 42; } \ No newline at end of file diff --git a/tests/isolated_headers/good_proj_inl/src/good2.hpp b/tests/isolated_headers/good_proj_inl/src/good2.hpp new file mode 100644 index 00000000..aa63cefb --- /dev/null +++ b/tests/isolated_headers/good_proj_inl/src/good2.hpp @@ -0,0 +1,10 @@ +#pragma once + +template +struct SomeType2 { + int doSomething(); + int doSomething2(); +}; + +#include +#include \ No newline at end of file diff --git a/tests/isolated_headers/good_proj_inl/src/good2.inl b/tests/isolated_headers/good_proj_inl/src/good2.inl new file mode 100644 index 00000000..a6ed7ff1 --- /dev/null +++ b/tests/isolated_headers/good_proj_inl/src/good2.inl @@ -0,0 +1,2 @@ +template +int SomeType2::doSomething() { return sizeof(T); } \ No newline at end of file diff --git a/tests/isolated_headers/good_proj_inl/src/good2.ipp b/tests/isolated_headers/good_proj_inl/src/good2.ipp new file mode 100644 index 00000000..b8ea6e85 --- /dev/null +++ b/tests/isolated_headers/good_proj_inl/src/good2.ipp @@ -0,0 +1,2 @@ +template +int SomeType2::doSomething2() { return 42; } \ No newline at end of file diff --git a/tests/isolated_headers/test_isolated_headers.py b/tests/isolated_headers/test_isolated_headers.py new file mode 100644 index 00000000..c6e8559e --- /dev/null +++ b/tests/isolated_headers/test_isolated_headers.py @@ -0,0 +1,22 @@ +import pytest +import subprocess + +from bpt_ci.testing import ProjectOpener, error + + +def test_dependent_src_header_fails(project_opener: ProjectOpener) -> None: + proj = project_opener.open('bad_proj') + ### XXX: Create a separate error-handling path for header-check failures and don't reuse the compile_failure error + with error.expect_error_marker_re('compile-failed'): + proj.build() + + +def test_dependent_include_header_fails(project_opener: ProjectOpener) -> None: + proj = project_opener.open('bad_proj_include') + with error.expect_error_marker_re('compile-failed'): + proj.build() + + +def test_dependent_inl_or_ipp_succeeds(project_opener: ProjectOpener) -> None: + proj = project_opener.open('good_proj_inl') + proj.build() diff --git a/tests/projects/simple-cmake/CMakeLists.txt b/tests/projects/simple-cmake/CMakeLists.txt index 9eacf8cd..44706c35 100644 --- a/tests/projects/simple-cmake/CMakeLists.txt +++ b/tests/projects/simple-cmake/CMakeLists.txt @@ -4,4 +4,4 @@ project(TestProject) include(${PROJECT_BINARY_DIR}/libraries.cmake) add_executable(app main.cpp) -target_link_libraries(app PRIVATE test::foo) +target_link_libraries(app PRIVATE foo::foo) diff --git a/tests/projects/simple-cmake/main.cpp b/tests/projects/simple-cmake/main.cpp index 1a37f016..ad3bd279 100644 --- a/tests/projects/simple-cmake/main.cpp +++ b/tests/projects/simple-cmake/main.cpp @@ -1,3 +1,3 @@ #include -int main() { say_hello(); } \ No newline at end of file +int main() { foo_fun(); } \ No newline at end of file diff --git a/tests/projects/simple/bpt.yaml b/tests/projects/simple/bpt.yaml new file mode 100644 index 00000000..5fef815f --- /dev/null +++ b/tests/projects/simple/bpt.yaml @@ -0,0 +1,4 @@ +{ + "name": "foo", + "version": "1.2.3" +} \ No newline at end of file diff --git a/tests/projects/simple/library.jsonc b/tests/projects/simple/library.jsonc deleted file mode 100644 index 07a21daa..00000000 --- a/tests/projects/simple/library.jsonc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "foo" -} \ No newline at end of file diff --git a/tests/projects/simple/package.json5 b/tests/projects/simple/package.json5 deleted file mode 100644 index 5ccccdc0..00000000 --- a/tests/projects/simple/package.json5 +++ /dev/null @@ -1,5 +0,0 @@ -{ - name: 'foo', - version: '1.2.3', - "namespace": "test", -} \ No newline at end of file diff --git a/tests/projects/tweaks/bpt.yaml b/tests/projects/tweaks/bpt.yaml new file mode 100644 index 00000000..1ea3da38 --- /dev/null +++ b/tests/projects/tweaks/bpt.yaml @@ -0,0 +1 @@ +{ name: "tweakable", version: "1.2.3" } diff --git a/tests/projects/tweaks/library.jsonc b/tests/projects/tweaks/library.jsonc deleted file mode 100644 index 07a21daa..00000000 --- a/tests/projects/tweaks/library.jsonc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "foo" -} \ No newline at end of file diff --git a/tests/projects/tweaks/package.json5 b/tests/projects/tweaks/package.json5 deleted file mode 100644 index 78eaa6de..00000000 --- a/tests/projects/tweaks/package.json5 +++ /dev/null @@ -1,5 +0,0 @@ -{ - name: 'tweakable', - version: '1.2.3', - "namespace": "test", -} \ No newline at end of file diff --git a/tests/self-uses-rejected/test_self_uses.py b/tests/self-uses-rejected/test_self_uses.py new file mode 100644 index 00000000..24c25371 --- /dev/null +++ b/tests/self-uses-rejected/test_self_uses.py @@ -0,0 +1,44 @@ +from bpt_ci.testing import Project, error + + +def test_self_referential_uses_cycle_fails(tmp_project: Project) -> None: + tmp_project.bpt_yaml = { + 'name': + 'mylib', + 'version': + '0.1.0', + 'libraries': [{ + 'name': 'liba', + 'path': 'libs/liba', + 'using': ['libb'], + }, { + 'name': 'libb', + 'path': 'libs/libb', + 'using': ['liba'], + }], + } + liba = tmp_project.lib('liba') + liba.write('src/a.cpp', 'int answera() { return 42; }') + + libb = tmp_project.lib('libb') + libb.write('src/b.cpp', 'int answerb() { return 24; }') + + with error.expect_error_marker_re(r'library-json-cyclic-dependency'): + tmp_project.build() + + +def test_self_referential_uses_for_libs_fails(tmp_project: Project) -> None: + tmp_project.bpt_yaml = { + 'name': 'mylib', + 'version': '0.1.0', + 'libraries': [{ + 'name': 'liba', + 'path': 'libs/liba', + 'using': ['liba'], + }] + } + lib = tmp_project.lib('liba') + lib.write('src/a.cpp', 'int answer() { return 42; }') + + with error.expect_error_marker_re(r'library-json-cyclic-dependency'): + tmp_project.build() diff --git a/tests/test_basics.py b/tests/test_basics.py index 65c86198..a8ea677b 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -1,20 +1,23 @@ -from subprocess import CalledProcessError +from pathlib import Path import time +from subprocess import CalledProcessError import pytest - -from dds_ci import paths -from dds_ci.testing.error import expect_error_marker -from dds_ci.testing import Project, PackageJSON +from bpt_ci import paths +from bpt_ci.bpt import BPTWrapper +from bpt_ci.testing import Project, ProjectYAML +from bpt_ci.testing.error import expect_error_marker +from bpt_ci.testing.fixtures import ProjectOpener +from bpt_ci.testing.fs import render_into def test_build_empty(tmp_project: Project) -> None: - """Check that dds is okay with building an empty project directory""" + """Check that bpt is okay with building an empty project directory""" tmp_project.build() def test_lib_with_app_only(tmp_project: Project) -> None: - """Test that dds can build a simple application""" + """Test that bpt can build a simple application""" tmp_project.write('src/foo.main.cpp', r'int main() {}') tmp_project.build() assert (tmp_project.build_root / f'foo{paths.EXE_SUFFIX}').is_file() @@ -22,7 +25,7 @@ def test_lib_with_app_only(tmp_project: Project) -> None: def test_build_simple(tmp_project: Project) -> None: """ - Test that dds can build a simple library, and handles rebuilds correctly. + Test that bpt can build a simple library, and handles rebuilds correctly. """ # Build a bad project tmp_project.write('src/f.cpp', 'syntax error') @@ -40,19 +43,17 @@ def test_build_simple(tmp_project: Project) -> None: def test_simple_lib(tmp_project: Project) -> None: """ - Test that dds can build a simple library withsome actual content, and that + Test that bpt can build a simple library withsome actual content, and that the manifest files will affect the output name. """ tmp_project.write('src/foo.cpp', 'int the_answer() { return 42; }') - tmp_project.package_json = { - 'name': 'TestProject', + tmp_project.bpt_yaml = { + 'name': 'test-project', 'version': '0.0.0', - 'namespace': 'test', } - tmp_project.library_json = {'name': 'TestLibrary'} tmp_project.build() - assert (tmp_project.build_root / 'compile_commands.json').is_file() - assert list(tmp_project.build_root.glob('libTestLibrary.*')) != [] + assert (tmp_project.build_root / 'compile_commands.json').is_file(), 'compdb was not created' + assert list(tmp_project.build_root.glob('libtest-project.*')) != [], 'No archive was created' def test_lib_with_just_test(tmp_project: Project) -> None: @@ -67,20 +68,179 @@ def test_lib_with_failing_test(tmp_project: Project) -> None: tmp_project.build() -TEST_PACKAGE: PackageJSON = { +def test_error_enoent_toolchain(tmp_project: Project) -> None: + with expect_error_marker('bad-toolchain'): + tmp_project.build(toolchain='no-such-file', fixup_toolchain=False) + + +def test_invalid_names(tmp_project: Project) -> None: + tmp_project.bpt_yaml = {'name': 'test', 'version': '1.2.3', 'dependencies': [{'dep': 'invalid name@1.2.3'}]} + with expect_error_marker('invalid-pkg-dep-name'): + tmp_project.build() + with expect_error_marker('invalid-pkg-dep-name'): + tmp_project.pkg_create() + with expect_error_marker('invalid-dep-shorthand'): + tmp_project.bpt.build_deps(['invalid name@1.2.3']) + + tmp_project.bpt_yaml['name'] = 'invalid name' + tmp_project.bpt_yaml['dependencies'] = [] + with expect_error_marker('invalid-name'): + tmp_project.build() + with expect_error_marker('invalid-name'): + tmp_project.pkg_create() + + +TEST_PACKAGE: ProjectYAML = { 'name': 'test-pkg', 'version': '0.2.2', - 'namespace': 'test', } def test_empty_with_pkg_json(tmp_project: Project) -> None: - tmp_project.package_json = TEST_PACKAGE + tmp_project.bpt_yaml = TEST_PACKAGE tmp_project.build() def test_empty_sdist_create(tmp_project: Project) -> None: - tmp_project.package_json = TEST_PACKAGE + tmp_project.bpt_yaml = TEST_PACKAGE tmp_project.pkg_create() - assert tmp_project.build_root.joinpath('test-pkg@0.2.2.tar.gz').is_file(), \ + assert tmp_project.build_root.joinpath('test-pkg@0.2.2~1.tar.gz').is_file(), \ 'The expected sdist tarball was not generated' + + +def test_project_with_meta(tmp_project: Project) -> None: + tmp_project.bpt_yaml = { + 'name': 'foo', + 'version': '1.2.3', + 'license': 'MIT-1.2.3', + } + with expect_error_marker('invalid-spdx'): + tmp_project.build() + tmp_project.bpt_yaml = { + 'name': 'foo', + 'version': '1.2.3', + 'license': 'MIT', # A valid license string + } + tmp_project.build() + + +def test_link_interdep(project_opener: ProjectOpener) -> None: + proj = project_opener.render( + 'test-interdep', { + 'foo': { + 'src': { + 'foo.test.cpp': + r''' + #include + + extern int bar_func(); + + int main() { + if (bar_func() != 1729) { + std::terminate(); + } + } + ''' + }, + }, + 'bar': { + 'src': { + 'bar.cpp': + r''' + int bar_func() { + return 1729; + } + ''', + }, + }, + }) + proj.bpt_yaml = { + 'name': 'test', + 'version': '1.2.3', + 'libraries': [{ + 'path': 'foo', + 'name': 'foo', + }, { + 'path': 'bar', + 'name': 'bar', + }] + } + with expect_error_marker('link-failed'): + proj.build() + + # Update with a 'using' of the library that was required: + proj.bpt_yaml['libraries'][0]['using'] = ['bar'] + print(proj.bpt_yaml) + # Build is okay now + proj.build() + + +def test_build_other_dir(project_opener: ProjectOpener, tmp_path: Path, bpt: BPTWrapper): + render_into( + tmp_path, { + 'build-other-dir': { + 'src': { + 'foo/': { + 'foo.hpp': + r''' + extern int get_value(); + ''', + 'foo.cpp': + r''' + #include + int get_value() { return 42; } + ''', + 'foo.test.cpp': + r''' + #include + int main() { + return get_value() != 42; + } + ''' + } + } + } + }) + proj = Project(Path('./build-other-dir/'), bpt) + proj.build(cwd=tmp_path) + + +def test_build_with_explicit_libs_ignores_default_lib(project_opener: ProjectOpener, tmp_path: Path, bpt: BPTWrapper): + render_into( + tmp_path, { + 'src': { + 'bad.cpp': r''' + #error This file is invalid + ''' + }, + 'okay-lib': { + 'src': { + 'okay.cpp': r''' + // This file is okay + ''' + } + } + }) + proj = Project(tmp_path, bpt) + with expect_error_marker('compile-failed'): + proj.build() + + proj.bpt_yaml = { + 'name': 'mine', + 'version': '1.2.3', + } + with expect_error_marker('compile-failed'): + proj.build() + + # Setting an explicit library list does not generate the default library + proj.bpt_yaml['libraries'] = [{ + 'name': 'something', + 'path': 'okay-lib', + }] + proj.build() + + +def test_new_then_build(bpt: BPTWrapper, tmp_path: Path) -> None: + bpt.run(['new', 'test-project', '--dir', tmp_path, '--split-src-include=no']) + proj = Project(tmp_path, bpt) + proj.build() diff --git a/tests/test_build_deps.py b/tests/test_build_deps.py index 8c264521..6bc5e6c8 100644 --- a/tests/test_build_deps.py +++ b/tests/test_build_deps.py @@ -2,84 +2,239 @@ import pytest -from dds_ci.testing import RepoServer, Project, ProjectOpener -from dds_ci import proc, toolchain - -SIMPLE_CATALOG = { - "packages": { - "neo-fun": { - "0.3.0": { - "remote": { - "git": { - "url": "https://github.com/vector-of-bool/neo-fun.git", - "ref": "0.3.0" +from bpt_ci.testing import Project, ProjectOpener +from bpt_ci.testing.error import expect_error_marker +from bpt_ci.testing.fs import DirRenderer +from bpt_ci.testing.repo import CRSRepo, CRSRepoFactory, make_simple_crs +from bpt_ci import proc + + +@pytest.fixture(scope='session') +def bd_test_repo(crs_repo_factory: CRSRepoFactory, dir_renderer: DirRenderer) -> CRSRepo: + repo = crs_repo_factory('bd-repo') + repo.import_([ + dir_renderer.get_or_render( + 'pkg_1', { + 'pkg.json': json.dumps(make_simple_crs('foo', '1.2.3')), + 'src': { + 'foo.hpp': + '#pragma once\nextern void foo_fun();', + 'foo.cpp': + r''' + #include + + #include "./foo.hpp" + + void foo_fun() { + puts("Hello, from foo library!\n"); } + ''' } - } - } - } -} + }), + dir_renderer.get_or_render( + 'pkg_2', { + 'pkg.json': json.dumps(make_simple_crs('foo', '2.0.0')), + 'src': { + 'foo.hpp': + '#pragma once\n extern void bar_fun();', + 'foo.cpp': + r''' + #include + + #include "./foo.hpp" + + void bar_fun() { + puts("Hello, new foo library!\n"); + } + ''' + } + }), + dir_renderer.get_or_render( + 'pkg_3', { + 'pkg.json': json.dumps(make_simple_crs('foo', '1.2.8')), + 'src': { + 'foo.hpp': + '#pragma once\n extern void foo_fun();', + 'foo.cpp': + r''' + #include + #include "./foo.hpp" + + void foo_fun() { + puts("foo 1.2.8!\n"); + } + ''' + } + }), + dir_renderer.get_or_render( + 'pkg_4', { + 'pkg.json': json.dumps(make_simple_crs('foo', '1.3.4')), + 'src': { + 'foo.hpp': + '#pragma once\n extern void foo_fun();', + 'foo.cpp': + r''' + #include + #include "./foo.hpp" + + void foo_fun() { + puts("foo 1.3.4!\n"); + } + ''' + } + }), + ]) + return repo @pytest.fixture() -def test_repo(http_repo: RepoServer) -> RepoServer: - http_repo.import_json_data(SIMPLE_CATALOG) - return http_repo +def bd_project(tmp_project: Project) -> Project: + tmp_project.bpt.crs_cache_dir = tmp_project.root / '_crs' + return tmp_project -@pytest.fixture() -def test_project(tmp_project: Project, test_repo: RepoServer) -> Project: - tmp_project.dds.repo_add(test_repo.url) - return tmp_project +def test_cli(bd_test_repo: CRSRepo, bd_project: Project) -> None: + bd_project.bpt.build_deps(['foo@1.2.3'], repos=[bd_test_repo.path]) + assert bd_project.root.joinpath('_deps/foo@1.2.3~1').is_dir() + assert bd_project.root.joinpath('_built.json').is_file() -def test_from_file(test_project: Project) -> None: +def test_cli_missing(bd_test_repo: CRSRepo, bd_project: Project) -> None: + with expect_error_marker('no-dependency-solution'): + bd_project.bpt.build_deps(['no-such-pkg@4.5.6']) + + +def test_from_file(bd_test_repo: CRSRepo, bd_project: Project) -> None: """build-deps using a file listing deps""" - test_project.write('deps.json5', json.dumps({'depends': ['neo-fun+0.3.0']})) - test_project.dds.build_deps(['-d', 'deps.json5']) - assert test_project.root.joinpath('_deps/neo-fun@0.3.0').is_dir() - assert test_project.root.joinpath('_deps/_libman/neo-fun.lmp').is_file() - assert test_project.root.joinpath('_deps/_libman/neo/fun.lml').is_file() - assert test_project.root.joinpath('INDEX.lmi').is_file() + bd_project.write('deps.json5', json.dumps({'dependencies': ['foo+0.3.0']})) + bd_project.bpt.build_deps(['-d', 'deps.json5'], repos=[bd_test_repo.path]) + assert bd_project.root.joinpath('_deps/foo@1.2.3~1').is_dir() + assert bd_project.root.joinpath('_built.json').is_file() -def test_from_cmd(test_project: Project) -> None: - """build-deps using a command-line listing""" - test_project.dds.build_deps(['neo-fun=0.3.0']) - assert test_project.root.joinpath('_deps/neo-fun@0.3.0').is_dir() - assert test_project.root.joinpath('_deps/_libman/neo-fun.lmp').is_file() - assert test_project.root.joinpath('_deps/_libman/neo/fun.lml').is_file() - assert test_project.root.joinpath('INDEX.lmi').is_file() +def test_from_file_missing(bd_project: Project) -> None: + bd_project.write('deps.json5', json.dumps({'dependencies': ['no-such-pkg@9.3.1']})) + with expect_error_marker('no-dependency-solution'): + bd_project.bpt.build_deps(['-d', 'deps.json5']) -def test_multiple_deps(test_project: Project) -> None: +def test_multiple_deps(bd_test_repo: CRSRepo, bd_project: Project) -> None: """build-deps with multiple deps resolves to a single version""" - test_project.dds.build_deps(['neo-fun^0.2.0', 'neo-fun~0.3.0']) - assert test_project.root.joinpath('_deps/neo-fun@0.3.0').is_dir() - assert test_project.root.joinpath('_deps/_libman/neo-fun.lmp').is_file() - assert test_project.root.joinpath('_deps/_libman/neo/fun.lml').is_file() - assert test_project.root.joinpath('INDEX.lmi').is_file() - + bd_project.bpt.build_deps(['foo@1.2.3', 'foo@1.2.6'], repos=[bd_test_repo.path]) + assert bd_project.root.joinpath('_deps/foo@1.2.8~1').is_dir() + assert bd_project.root.joinpath('_built.json').is_file() -def test_cmake_simple(project_opener: ProjectOpener) -> None: - proj = project_opener.open('projects/simple') - proj.dds.pkg_import(proj.root) - cm_proj_dir = project_opener.test_dir / 'projects/simple-cmake' +def test_cmake_simple(project_opener: ProjectOpener, bd_test_repo: CRSRepo) -> None: + proj = project_opener.open('projects/simple-cmake') proj.build_root.mkdir(exist_ok=True, parents=True) - proj.dds.run( + proj.bpt.crs_cache_dir = proj.build_root / '_crs' + proj.bpt.build_deps( [ - 'build-deps', - proj.dds.cache_dir_arg, 'foo@1.2.3', - ('-t', ':gcc' if 'gcc' in toolchain.get_default_toolchain().name else ':msvc'), - f'--cmake=libraries.cmake', + f'--cmake={proj.build_root}/libraries.cmake', ], - cwd=proj.build_root, + repos=[bd_test_repo.path], ) try: - proc.check_run(['cmake', '-S', cm_proj_dir, '-B', proj.build_root]) + proc.check_run(['cmake', '-S', proj.root, '-B', proj.build_root]) except FileNotFoundError: assert False, 'Running the integration tests requires a CMake executable' proc.check_run(['cmake', '--build', proj.build_root]) + + +def test_cmake_transitive(bd_project: Project, tmp_crs_repo: CRSRepo, dir_renderer: DirRenderer) -> None: + # yapf: disable + libs = dir_renderer.get_or_render( + 'libs', + { + 'foo': { + 'pkg.json': json.dumps({ + 'schema-version': 0, + 'name': 'foo', + 'version': '1.2.3', + 'pkg-version': 1, + 'libraries': [{ + 'name': 'foo', + 'path': '.', + 'test-using': [], + 'using': [], + 'test-dependencies': [], + 'dependencies': [], + }] + }), + 'src': { + 'foo.hpp': r''' + #pragma once + namespace foo{int value();} + ''', + 'foo.cpp': r''' + #include "./foo.hpp" + int foo::value(){ return 42; } + ''', + } + }, + 'bar': { + 'pkg.json': json.dumps({ + 'schema-version': 0, + 'name': 'bar', + 'version': '1.2.3', + 'pkg-version': 1, + 'libraries': [{ + 'name': 'bar', + 'path': '.', + 'using': [], + 'test-using': [], + 'dependencies': [{ + 'name': 'foo', + 'versions': [{'low': '1.2.3', 'high': '1.2.4'}], + 'using': ['foo'], + }], + 'test-dependencies': [], + }] + }), + 'src': { + 'bar.hpp': r''' + #pragma once + namespace bar{int value();} + ''', + 'bar.cpp': r''' + #include "./bar.hpp" + #include + int bar::value() { return foo::value() * 2; } + ''', + } + } + }, + ) + # yapf: enable + tmp_crs_repo.import_((libs / 'foo', libs / 'bar')) + + bd_project.write( + 'CMakeLists.txt', r''' + cmake_minimum_required(VERSION 3.0) + project(TestProject) + + include(CTest) + include(libraries.cmake) + add_executable(app app.cpp) + target_link_libraries(app PRIVATE bar::bar) + add_test(app app) + ''') + bd_project.write( + 'app.cpp', r''' + #include + #include + #include + + int main() { + std::cout << "Bar value is " << bar::value() << '\n'; + assert(bar::value() == 84); + } + ''') + + bd_project.bpt.build_deps(['bar@1.2.3', f'--cmake=libraries.cmake'], repos=[tmp_crs_repo.path]) + proc.check_run(['cmake', '-S', bd_project.root, '-B', bd_project.build_root]) + proc.check_run(['cmake', '--build', bd_project.build_root]) + proc.check_run(['ctest'], cwd=bd_project.build_root) diff --git a/tests/test_compile_deps.py b/tests/test_compile_deps.py index d8fcab28..4e3e46b7 100644 --- a/tests/test_compile_deps.py +++ b/tests/test_compile_deps.py @@ -2,8 +2,8 @@ import pytest -from dds_ci.testing import ProjectOpener, Project -from dds_ci import proc, paths +from bpt_ci.testing import ProjectOpener, Project +from bpt_ci import proc, paths ## ############################################################################# ## ############################################################################# diff --git a/tests/test_compile_file.py b/tests/test_compile_file.py index b450d1ca..6390c0dc 100644 --- a/tests/test_compile_file.py +++ b/tests/test_compile_file.py @@ -1,22 +1,22 @@ -import subprocess - -import pytest import time -from dds_ci.testing import Project +from bpt_ci.testing import Project, error + + +def test_compile_file_missing(tmp_project: Project) -> None: + with error.expect_error_marker('nonesuch-compile-file'): + tmp_project.compile_file('src/answer.cpp') def test_simple_compile_file(tmp_project: Project) -> None: """ Check that changing a source file will update the resulting application. """ - with pytest.raises(subprocess.CalledProcessError): - tmp_project.compile_file('src/answer.cpp') tmp_project.write('src/answer.cpp', 'int get_answer() { return 42; }') # No error: tmp_project.compile_file('src/answer.cpp') # Fail: time.sleep(1) # Sleep long enough to register a file change tmp_project.write('src/answer.cpp', 'int get_answer() { return "How many roads must a man walk down?"; }') - with pytest.raises(subprocess.CalledProcessError): + with error.expect_error_marker('compile-file-failed'): tmp_project.compile_file('src/answer.cpp') diff --git a/tests/test_drivers/catch/custom-runner/package.json5 b/tests/test_drivers/catch/custom-runner/package.json5 deleted file mode 100644 index 4102183e..00000000 --- a/tests/test_drivers/catch/custom-runner/package.json5 +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "test", - "version": "0.0.0", - "namespace": "test", - "test_driver": "Catch", -} \ No newline at end of file diff --git a/tests/test_drivers/catch/custom-runner/src/testlib/calc.hpp b/tests/test_drivers/catch/custom-runner/src/testlib/calc.hpp deleted file mode 100644 index efc503e5..00000000 --- a/tests/test_drivers/catch/custom-runner/src/testlib/calc.hpp +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -namespace stuff { - -int calculate(int a, int b) { - int result = a + b; - if (result == 42) { - return result; - } else { - return 42; - } -} - -} // namespace stuff \ No newline at end of file diff --git a/tests/test_drivers/catch/custom-runner/src/testlib/calc.test.cpp b/tests/test_drivers/catch/custom-runner/src/testlib/calc.test.cpp deleted file mode 100644 index ef843c95..00000000 --- a/tests/test_drivers/catch/custom-runner/src/testlib/calc.test.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#define CATCH_CONFIG_RUNNER -#include - -#include - -TEST_CASE("A simple test case") { - CHECK_FALSE(false); - CHECK(2 == 2); - CHECK(1 != 4); - CHECK(stuff::calculate(3, 11) == 42); -} - -int main(int argc, char** argv) { - // We provide our own runner - return Catch::Session().run(argc, argv); -} \ No newline at end of file diff --git a/tests/test_drivers/catch/main/package.json5 b/tests/test_drivers/catch/main/package.json5 deleted file mode 100644 index 23063cd0..00000000 --- a/tests/test_drivers/catch/main/package.json5 +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "test", - "version": "0.0.0", - "namespace": "test", - "test_driver": "Catch-Main", -} \ No newline at end of file diff --git a/tests/test_drivers/catch/main/src/testlib/calc.hpp b/tests/test_drivers/catch/main/src/testlib/calc.hpp deleted file mode 100644 index efc503e5..00000000 --- a/tests/test_drivers/catch/main/src/testlib/calc.hpp +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -namespace stuff { - -int calculate(int a, int b) { - int result = a + b; - if (result == 42) { - return result; - } else { - return 42; - } -} - -} // namespace stuff \ No newline at end of file diff --git a/tests/test_drivers/catch/main/src/testlib/calc.test.cpp b/tests/test_drivers/catch/main/src/testlib/calc.test.cpp deleted file mode 100644 index 013ba0a5..00000000 --- a/tests/test_drivers/catch/main/src/testlib/calc.test.cpp +++ /dev/null @@ -1,10 +0,0 @@ -#include - -#include - -TEST_CASE("A simple test case") { - CHECK_FALSE(false); - CHECK(2 == 2); - CHECK(1 != 4); - CHECK(stuff::calculate(3, 11) == 42); -} \ No newline at end of file diff --git a/tests/test_drivers/catch/test_catch.py b/tests/test_drivers/catch/test_catch.py deleted file mode 100644 index 7602efc6..00000000 --- a/tests/test_drivers/catch/test_catch.py +++ /dev/null @@ -1,18 +0,0 @@ -from dds_ci import proc, paths -from dds_ci.testing import ProjectOpener - - -def test_main(project_opener: ProjectOpener) -> None: - proj = project_opener.open('main') - proj.build() - test_exe = proj.build_root.joinpath('test/testlib/calc' + paths.EXE_SUFFIX) - assert test_exe.is_file() - assert proc.run([test_exe]).returncode == 0 - - -def test_custom(project_opener: ProjectOpener) -> None: - proj = project_opener.open('custom-runner') - proj.build() - test_exe = proj.build_root.joinpath('test/testlib/calc' + paths.EXE_SUFFIX) - assert test_exe.is_file() - assert proc.run([test_exe]).returncode == 0 diff --git a/tests/test_pkg.py b/tests/test_pkg.py index e8f31bc8..04f947b7 100644 --- a/tests/test_pkg.py +++ b/tests/test_pkg.py @@ -1,12 +1,13 @@ +import json +import tarfile import pytest from pathlib import Path -from typing import Tuple -import subprocess + import platform -from dds_ci import proc -from dds_ci.dds import DDSWrapper -from dds_ci.testing import ProjectOpener, Project, error +from bpt_ci.bpt import BPTWrapper +from bpt_ci.testing import ProjectOpener, Project, error, CRSRepo +from bpt_ci.util import read_tarfile_member @pytest.fixture() @@ -17,7 +18,7 @@ def test_project(project_opener: ProjectOpener) -> Project: def test_create_pkg(test_project: Project, tmp_path: Path) -> None: # Create in the default location test_project.pkg_create() - sd_dir = test_project.build_root / 'foo@1.2.3.tar.gz' + sd_dir = test_project.build_root / 'foo@1.2.3~1.tar.gz' assert sd_dir.is_file(), 'Did not create an sdist in the default location' # Create in a different location dest = tmp_path / 'dummy.tar.gz' @@ -25,82 +26,138 @@ def test_create_pkg(test_project: Project, tmp_path: Path) -> None: assert dest.is_file(), 'Did not create an sdist in the new location' -@pytest.fixture() -def _test_pkg(test_project: Project) -> Tuple[Path, Project]: - repo_content_path = test_project.dds.repo_dir / 'foo@1.2.3' - assert not repo_content_path.is_dir() +def test_create_pkg_already_exists_fails(test_project: Project) -> None: + test_project.pkg_create() + with error.expect_error_marker('sdist-already-exists'): + test_project.pkg_create() + + +def test_create_pkg_already_exists_fails_with_explicit_fail(test_project: Project) -> None: + test_project.pkg_create() + with error.expect_error_marker('sdist-already-exists'): + test_project.pkg_create(if_exists='fail') + + +def test_create_pkg_already_exists_succeeds_with_ignore(test_project: Project) -> None: test_project.pkg_create() - assert not repo_content_path.is_dir() - return test_project.build_root / 'foo@1.2.3.tar.gz', test_project - - -def test_import_sdist_archive(_test_pkg: Tuple[Path, Project]) -> None: - sdist, project = _test_pkg - repo_content_path = project.dds.repo_dir / 'foo@1.2.3' - project.dds.pkg_import(sdist) - assert repo_content_path.is_dir(), \ - 'The package did not appear in the local cache' - assert repo_content_path.joinpath('library.jsonc').is_file(), \ - 'The package\'s library.jsonc did not get imported' - # Excluded file will not be in the sdist: - assert not repo_content_path.joinpath('other-file.txt').is_file(), \ - 'Non-package content appeared in the package cache' - - -@pytest.mark.skipif(platform.system() == 'Windows', - reason='Windows has trouble reading packages from stdin. Need to investigate.') -def test_import_sdist_stdin(_test_pkg: Tuple[Path, Project]) -> None: - sdist, project = _test_pkg - pipe = subprocess.Popen( - list(proc.flatten_cmd([ - project.dds.path, - project.dds.cache_dir_arg, - 'pkg', - 'import', - '--stdin', - ])), - stdin=subprocess.PIPE, - ) - assert pipe.stdin - with sdist.open('rb') as sdist_bin: - buf = sdist_bin.read(1024) - while buf: - pipe.stdin.write(buf) - buf = sdist_bin.read(1024) - pipe.stdin.close() - - rc = pipe.wait() - assert rc == 0, 'Subprocess failed' - _check_import(project.dds.repo_dir / 'foo@1.2.3') - - -def test_import_sdist_dir(test_project: Project) -> None: - test_project.dds.run(['pkg', 'import', test_project.dds.cache_dir_arg, test_project.root]) - _check_import(test_project.dds.repo_dir / 'foo@1.2.3') - - -def _check_import(repo_content_path: Path) -> None: - assert repo_content_path.is_dir(), \ - 'The package did not appear in the local cache' - assert repo_content_path.joinpath('library.jsonc').is_file(), \ - 'The package\'s library.jsonc did not get imported' - # Excluded file will not be in the sdist: - assert not repo_content_path.joinpath('other-file.txt').is_file(), \ - 'Non-package content appeared in the package cache' + test_project.pkg_create(if_exists='ignore') + + +def test_create_pkg_already_exists_succeeds_with_replace(test_project: Project) -> None: + sd_path: Path = test_project.build_root / 'foo@1.2.3~1.tar.gz' + test_project.build_root.mkdir(exist_ok=True, parents=True) + sd_path.touch() + test_project.pkg_create(if_exists='replace') + assert sd_path.stat().st_size > 0, 'Did not replace the existing file' def test_sdist_invalid_project(tmp_project: Project) -> None: - with error.expect_error_marker('no-package-json5'): + with error.expect_error_marker('no-pkg-meta-files'): tmp_project.pkg_create() @pytest.mark.skipif(platform.system() != 'Linux', reason='We know this fails on Linux') -def test_sdist_unreadable_dir(dds: DDSWrapper) -> None: - with error.expect_error_marker('failed-package-json5-scan'): - dds.run(['pkg', 'create', '--project=/root']) +def test_sdist_unreadable_dir(bpt: BPTWrapper) -> None: + with error.expect_error_marker('sdist-open-fail-generic'): + bpt.run(['pkg', 'create', '--project=/root']) + + +def test_sdist_invalid_yml(tmp_project: Project) -> None: + tmp_project.write('bpt.yaml', '[[') + with error.expect_error_marker('package-yaml-parse-error'): + tmp_project.pkg_create() + +def test_pkg_search(tmp_crs_repo: CRSRepo, tmp_project: Project) -> None: + with error.expect_error_marker('pkg-search-no-result'): + tmp_project.bpt.run(['pkg', 'search', 'test-pkg', '-r', tmp_crs_repo.path]) + tmp_project.bpt_yaml = { + 'name': 'test-pkg', + 'version': '0.1.2', + } + tmp_crs_repo.import_(tmp_project.root) + # No error now: + tmp_project.bpt.run(['pkg', 'search', 'test-pkg', '-r', tmp_crs_repo.path]) + + +def test_pkg_spdx(tmp_project: Project) -> None: + tmp_project.bpt_yaml = { + 'name': 'foo', + 'version': '1.2.3', + 'license': 'MIT', + } + tmp_project.pkg_create() + tmp_project.bpt_yaml['license'] = 'bogus' + with error.expect_error_marker('invalid-spdx'): + tmp_project.pkg_create() -def test_sdist_invalid_json5(tmp_project: Project) -> None: - tmp_project.write('package.json5', 'bogus json5') - with error.expect_error_marker('package-json5-parse-error'): + dest_tgz = tmp_project.root / 'test.tgz' + tmp_project.bpt_yaml['license'] = 'MIT' + tmp_project.pkg_create(dest=dest_tgz) + with tarfile.open(dest_tgz) as tgz: + pkg_json = json.loads(tgz.extractfile('pkg.json').read()) + assert 'meta' in pkg_json + assert 'license' in pkg_json['meta'] + assert pkg_json['meta']['license'] == 'MIT' + + +def test_pkg_create_almost_valid(tmp_project: Project) -> None: + pkg_data = { + "name": + "catch2", + "version": + "2.13.6", + "pkg-version": + 1, + "libraries": [{ + "name": "catch2", + "path": ".", + "using": [], + "test-using": [], + "dependencies": [], + "test-dependencies": [] + }, { + "name": "main", + "path": "libs/main", + "test-dependencies": [], + "dependencies": [], + "test-using": [], + "using": [{ + "lib": "catch2" + }] + }], + "schema-version": + 0 + } + tmp_project.write('pkg.json', json.dumps(pkg_data)) + with error.expect_error_marker('invalid-pkg-json'): + tmp_project.build() + with error.expect_error_marker('invalid-pkg-json'): tmp_project.pkg_create() + + pkg_data['libraries'][1]['using'] = ['catch2'] + tmp_project.root.joinpath('libs/main').mkdir(parents=True) + tmp_project.write('pkg.json', json.dumps(pkg_data)) + tmp_project.build() + tmp_project.pkg_create() + + +def test_pkg_create_default_using(tmp_project: Project): + tmp_project.bpt_yaml = {'name': 'foo', 'version': '1.2.3', 'dependencies': ['bar@4.1.3']} + tmp_project.pkg_create() + pkg_json = json.loads(read_tarfile_member(tmp_project.build_root / 'foo@1.2.3~1.tar.gz', 'pkg.json')) + assert pkg_json['libraries'][0] == { + 'name': 'foo', + 'path': '.', + 'using': [], + 'test-using': [], + 'test-dependencies': [], + 'dependencies': [{ + 'name': 'bar', + 'versions': [{ + 'low': '4.1.3', + 'high': '5.0.0' + }], + 'using': ['bar'], + }] + } diff --git a/tests/test_pkg_db.py b/tests/test_pkg_db.py deleted file mode 100644 index be5a3f87..00000000 --- a/tests/test_pkg_db.py +++ /dev/null @@ -1,72 +0,0 @@ -from dds_ci.dds import DDSWrapper -from dds_ci.testing import Project, RepoServer, PackageJSON -from dds_ci.testing.error import expect_error_marker -from dds_ci.testing.http import HTTPRepoServerFactory, RepoServer - -import pytest - -NEO_SQLITE_PKG_JSON = { - 'packages': { - 'neo-sqlite3': { - '0.3.0': { - 'remote': { - 'git': { - 'url': 'https://github.com/vector-of-bool/neo-sqlite3.git', - 'ref': '0.3.0', - } - } - } - } - } -} - - -@pytest.fixture(scope='session') -def _test_repo(http_repo_factory: HTTPRepoServerFactory) -> RepoServer: - srv = http_repo_factory('test-pkg-db-repo') - srv.import_json_data(NEO_SQLITE_PKG_JSON) - return srv - - -def test_pkg_get(_test_repo: RepoServer, tmp_project: Project) -> None: - _test_repo.import_json_data(NEO_SQLITE_PKG_JSON) - tmp_project.dds.repo_add(_test_repo.url) - tmp_project.dds.pkg_get('neo-sqlite3@0.3.0') - assert tmp_project.root.joinpath('neo-sqlite3@0.3.0').is_dir() - assert tmp_project.root.joinpath('neo-sqlite3@0.3.0/package.jsonc').is_file() - - -def test_pkg_repo(_test_repo: RepoServer, tmp_project: Project) -> None: - dds = tmp_project.dds - dds.repo_add(_test_repo.url) - dds.run(['pkg', 'repo', dds.pkg_db_path_arg, 'ls']) - - -def test_pkg_repo_rm(_test_repo: RepoServer, tmp_project: Project) -> None: - _test_repo.import_json_data(NEO_SQLITE_PKG_JSON) - dds = tmp_project.dds - dds.repo_add(_test_repo.url) - # Okay: - tmp_project.dds.pkg_get('neo-sqlite3@0.3.0') - # Remove the repo: - dds.run(['pkg', dds.pkg_db_path_arg, 'repo', 'ls']) - dds.repo_remove(_test_repo.repo_name) - # Cannot double-remove a repo: - with expect_error_marker('repo-rm-no-such-repo'): - dds.repo_remove(_test_repo.repo_name) - # Now, fails: - with expect_error_marker('pkg-get-no-pkg-id-listing'): - tmp_project.dds.pkg_get('neo-sqlite3@0.3.0') - - -def test_pkg_search(_test_repo: RepoServer, tmp_project: Project) -> None: - _test_repo.import_json_data(NEO_SQLITE_PKG_JSON) - dds = tmp_project.dds - with expect_error_marker('pkg-search-no-result'): - dds.run(['pkg', dds.pkg_db_path_arg, 'search']) - dds.repo_add(_test_repo.url) - dds.run(['pkg', dds.pkg_db_path_arg, 'search']) - dds.run(['pkg', dds.pkg_db_path_arg, 'search', 'neo-sqlite3']) - dds.run(['pkg', dds.pkg_db_path_arg, 'search', 'neo-*']) - with expect_error_marker('pkg-search-no-result'): - dds.run(['pkg', dds.pkg_db_path_arg, 'search', 'nonexistent']) diff --git a/tests/test_pkg_solve.py b/tests/test_pkg_solve.py new file mode 100644 index 00000000..6956a532 --- /dev/null +++ b/tests/test_pkg_solve.py @@ -0,0 +1,282 @@ +from contextlib import contextmanager +from typing import Dict, NamedTuple, Sequence +import itertools +import pytest +import json + +from typing_extensions import TypedDict, Protocol + +from bpt_ci.testing import CRSRepo, Project, error +from bpt_ci.testing.fs import DirRenderer, TreeData +from bpt_ci.testing.repo import RepoCloner, make_simple_crs + +_RepoPackageLibraryItem_Opt = TypedDict( + '_RepoPackageLibraryItem_Opt', + { + 'using': Sequence[str], + 'dependencies': Sequence[str], + 'content': TreeData, + }, + total=False, +) +_RepoPackageLibraryItem_Required = TypedDict('_RepoPackageLibraryItem', {}) + + +@contextmanager +def expect_solve_fail(): + with error.expect_error_marker('no-dependency-solution'): + yield + + +class _RepoPackageLibraryItem(_RepoPackageLibraryItem_Opt, _RepoPackageLibraryItem_Required): + pass + + +_RepoPackageItem = TypedDict('_RepoPackageItem', { + 'libs': Dict[str, _RepoPackageLibraryItem], +}) +_RepoPackageVersionsDict = Dict[str, _RepoPackageItem] +_RepoPackagesDict = Dict[str, _RepoPackageVersionsDict] + +RepoSpec = TypedDict('RepoSpec', {'packages': _RepoPackagesDict}) + + +def _render_pkg_version(name: str, version: str, item: _RepoPackageItem) -> TreeData: + proj = { + 'name': + name, + 'version': + version, + 'libraries': [ + { + 'name': lib_name, + 'path': f'libs/{lib_name}', + 'using': lib.get('using', []), + 'dependencies': lib.get('dependencies', []), + } for lib_name, lib in item['libs'].items() # + ] + } + return { + 'bpt.yaml': json.dumps(proj), + 'libs': { + lib_name: lib.get('content', {}) + for lib_name, lib in item['libs'].items() # + }, + } + + +def _render_repospec(spec: RepoSpec) -> TreeData: + pkgs = spec['packages'] + pkg_pairs = pkgs.items() + triples = itertools.chain.from_iterable( # + ((pkg, version, vspec) for version, vspec in versions.items()) # + for pkg, versions in pkg_pairs) + return {f'{name}@{version}': _render_pkg_version(name, version, pkg) for name, version, pkg in triples} + + +class QuickRepo(NamedTuple): + repo: CRSRepo + + def pkg_solve(self, *pkgs: str) -> None: + bpt = self.repo.bpt.clone() + bpt.crs_cache_dir = self.repo.path / '_bpt-cache' + bpt.pkg_solve(repos=[self.repo.path], pkgs=pkgs) + + @property + def path(self): + """The path of the reqpo""" + return self.repo.path + + +class QuickRepoFactory(Protocol): + + def __call__(self, name: str, spec: RepoSpec, validate: bool = True) -> QuickRepo: + ... + + +@pytest.fixture(scope='session') +def make_quick_repo(dir_renderer: DirRenderer, session_empty_crs_repo: CRSRepo, + clone_repo: RepoCloner) -> QuickRepoFactory: + + def _make(name: str, spec: RepoSpec, validate: bool = True) -> QuickRepo: + repo = clone_repo(session_empty_crs_repo) + data = _render_repospec(spec) + content = dir_renderer.get_or_render(f'{name}-pkgs', data) + repo.import_(content.iterdir(), validate=False) + if validate: + repo.validate() + return QuickRepo(repo) + + return _make + + +@pytest.fixture(scope='session') +def solve_repo_1(make_quick_repo: QuickRepoFactory) -> QuickRepo: + # yapf: disable + return make_quick_repo( + 'test1', + validate=False, + spec={ + 'packages': { + 'foo': { + '1.3.1': {'libs': {'main': {'using': ['bar']}, 'bar': {}}} + }, + 'pkg-adds-library': { + '1.2.3': {'libs': {'main': {}}}, + '1.2.4': {'libs': {'main': {}, 'another': {}}}, + }, + 'pkg-conflicting-libset': { + '1.2.3': {'libs': {'main': {}}}, + '1.2.4': {'libs': {'another': {}}}, + }, + 'pkg-conflicting-libset-2': { + '1.2.3': {'libs': {'main': {'dependencies': ['nonesuch@1.2.3 using bleh']}}}, + '1.2.4': {'libs': {'nope': {}}}, + }, + 'pkg-transitive-no-such-lib': { + '1.2.3': {'libs': {'main': {'dependencies': ['foo@1.2.3 using no-such-lib']}}} + }, + 'pkg-with-unsolvable-version': { + '1.2.3': {'libs': {'main': {'dependencies': ['nonesuch@1.2.3 using bleh']}}}, + '1.2.4': {'libs': {'main': {}}}, + }, + 'pkg-with-unsolvable-library': { + '1.2.3': {'libs': {'goodlib': {}, 'badlib': {'dependencies': ['nonesuch@1.2.3 using bleh']}}}, + }, + + 'pkg-unsolvable-version-diamond': { + '1.2.3': {'libs': {'main': {'dependencies': ['ver-diamond-left=1.2.3 using main', 'ver-diamond-right=1.2.3 using main']}}}, + }, + 'ver-diamond-left': { + '1.2.3': {'libs': {'main': {'dependencies': ['ver-diamond-tip=2.0.0 using main']}}} + }, + 'ver-diamond-right': { + '1.2.3': {'libs': {'main': {'dependencies': ['ver-diamond-tip=3.0.0 using main']}}} + }, + 'ver-diamond-tip': { + '2.0.0': {'libs': {'main': {}}}, + '3.0.0': {'libs': {'main': {}}}, + }, + + 'pkg-unsolvable-lib-diamond': { + '1.2.3': {'libs': {'main': {'dependencies': ['lib-diamond-left=1.2.3 using main', 'lib-diamond-right=1.2.3 using main']}}}, + }, + 'lib-diamond-left': { + '1.2.3': {'libs': {'main': {'dependencies': ['lib-diamond-tip^2.0.0 using lib-foo']}}} + }, + 'lib-diamond-right': { + '1.2.3': {'libs': {'main': {'dependencies': ['lib-diamond-tip^2.0.0 using lib-bar']}}} + }, + 'lib-diamond-tip': { + '2.0.0': {'libs': {'lib-foo': {}}}, + '2.0.1': {'libs': {'lib-bar': {}}}, + }, + }, + }, + ) + # yapf: enable + + +def test_solve_1(solve_repo_1: QuickRepo) -> None: + solve_repo_1.pkg_solve('foo@1.3.1 using main') + + +def test_solve_upgrade_pkg_version(make_quick_repo: QuickRepoFactory, tmp_project: Project, + dir_renderer: DirRenderer) -> None: + ''' + Test that bpt will pull a new copy of a package if its pkg_version is updated, + even if the version proper is not changed. + ''' + # yapf: disable + repo = make_quick_repo( + name='upgrade-pkg-revision', + spec={ + 'packages': { + 'foo': { + '1.2.3': { + 'libs': { + 'main': { + 'content': { + 'src': { + 'foo.hpp': '#error This file is bad\n' + } + } + } + } + } + } + } + } + ) + # yapf: enable + tmp_project.bpt_yaml = {'name': 'test-proj', 'version': '1.2.3', 'dependencies': ['foo@1.2.3 using main']} + tmp_project.write('src/file.cpp', '#include \n') + with error.expect_error_marker('compile-failed'): + tmp_project.build(repos=[repo.path]) + + # Create a new package that is good + new_foo_crs = make_simple_crs('foo', '1.2.3', pkg_version=2) + new_foo_crs['libraries'][0]['name'] = 'main' + new_foo = dir_renderer.get_or_render('new-foo', { + 'pkg.json': json.dumps(new_foo_crs), + 'src': { + 'foo.hpp': '#pragma once\n inline int get_value() { return 42; }\n' + } + }) + repo.repo.import_(new_foo) + tmp_project.build(repos=[repo.path]) + + +def test_solve_cand_missing_libs(solve_repo_1: QuickRepo) -> None: + """ + Check that we reject if the only candidate does not have the libraries that we need. + """ + with expect_solve_fail(): + solve_repo_1.pkg_solve('foo@1.3.1 using no-such-lib') + + +def test_solve_skip_for_req_libs(solve_repo_1: QuickRepo) -> None: + with expect_solve_fail(): + solve_repo_1.pkg_solve('pkg-adds-library=1.2.3 using another') + solve_repo_1.pkg_solve('pkg-adds-library^1.2.3 using another') + + +def test_solve_transitive_req_requires_lib_conflict(solve_repo_1: QuickRepo) -> None: + solve_repo_1.pkg_solve('pkg-conflicting-libset^1.2.3 using another') + + +def test_solve_unsolvable_version_diamond(solve_repo_1: QuickRepo): + with expect_solve_fail(): + solve_repo_1.pkg_solve('pkg-unsolvable-version-diamond@1.2.3 using main') + + +def test_solve_unsolvable_lib_diamond(solve_repo_1: QuickRepo): + with expect_solve_fail(): + solve_repo_1.pkg_solve('pkg-unsolvable-lib-diamond@1.2.3 using main') + + +def test_solve_ignore_unsolvable_libs(solve_repo_1: QuickRepo) -> None: + solve_repo_1.pkg_solve('pkg-with-unsolvable-library@1.2.3 using goodlib') + with expect_solve_fail(): + solve_repo_1.pkg_solve('pkg-with-unsolvable-library@1.2.3 using badlib') + + +def test_solve_skip_unsolvable_version(solve_repo_1: QuickRepo) -> None: + with expect_solve_fail(): + solve_repo_1.pkg_solve('pkg-with-unsolvable-version=1.2.3 using main') + # Okay: Just pull bar@1.2.4 + solve_repo_1.pkg_solve('pkg-with-unsolvable-version@1.2.3 using main') + + +def test_solve_fail_with_transitive_no_such_lib(solve_repo_1: QuickRepo) -> None: + # Error: No 'bar' has 'no-such-lib' + with expect_solve_fail(): + solve_repo_1.pkg_solve('pkg-transitive-no-such-lib@1.2.3 using main') + + +def test_solve_conflicting_libset_2(solve_repo_1: QuickRepo) -> None: + with expect_solve_fail(): + solve_repo_1.pkg_solve('pkg-conflicting-libset-2@1.2.3 using main') + with expect_solve_fail(): + solve_repo_1.pkg_solve('pkg-conflicting-libset-2@1.2.4 using main') + solve_repo_1.pkg_solve('pkg-conflicting-libset-2@1.2.3 using nope') diff --git a/tests/test_repo/test_repo.py b/tests/test_repo/test_repo.py new file mode 100644 index 00000000..11af2d36 --- /dev/null +++ b/tests/test_repo/test_repo.py @@ -0,0 +1,344 @@ +from pathlib import Path + +import tarfile +import pytest +import json +import sqlite3 + +from bpt_ci.bpt import BPTWrapper +from bpt_ci.paths import PROJECT_ROOT +from bpt_ci.testing.http import HTTPServerFactory +from bpt_ci.testing import Project +from bpt_ci.testing.error import expect_error_marker +from bpt_ci.testing.repo import CRSRepo, CRSRepoFactory, RepoCloner, make_simple_crs + + +def test_repo_init(tmp_crs_repo: CRSRepo) -> None: + assert tmp_crs_repo.path.joinpath('repo.db').is_file() + assert tmp_crs_repo.path.joinpath('repo.db.gz').is_file() + + +def test_repo_init_already(bpt: BPTWrapper, tmp_crs_repo: CRSRepo) -> None: + with expect_error_marker('repo-init-already-init'): + bpt.run(['repo', 'init', tmp_crs_repo.path, '--name=testing']) + + +def test_repo_init_ignore(bpt: BPTWrapper, tmp_crs_repo: CRSRepo) -> None: + before_time = tmp_crs_repo.path.joinpath('repo.db').stat().st_mtime + bpt.run(['repo', 'init', tmp_crs_repo.path, '--name=testing', '--if-exists=ignore']) + after_time = tmp_crs_repo.path.joinpath('repo.db').stat().st_mtime + assert before_time == after_time + + +def test_repo_init_replace(bpt: BPTWrapper, tmp_crs_repo: CRSRepo) -> None: + before_time = tmp_crs_repo.path.joinpath('repo.db').stat().st_mtime + bpt.run(['repo', 'init', tmp_crs_repo.path, '--name=testing', '--if-exists=replace']) + after_time = tmp_crs_repo.path.joinpath('repo.db').stat().st_mtime + assert before_time < after_time + + +def test_repo_import(bpt: BPTWrapper, tmp_crs_repo: CRSRepo, tmp_project: Project) -> None: + tmp_project.write( + 'pkg.json', + json.dumps({ + 'schema-version': + 0, + 'name': + 'meow', + 'version': + '1.2.3', + 'pkg-version': + 1, + 'libraries': [{ + 'name': 'test', + 'path': '.', + 'using': [], + 'test-using': [], + 'dependencies': [], + 'test-dependencies': [], + }], + })) + tmp_crs_repo.import_(tmp_project.root) + + +def test_repo_import1(bpt: BPTWrapper, tmp_crs_repo: CRSRepo) -> None: + tmp_crs_repo.import_(PROJECT_ROOT / 'data/simple.crs', validate=False) + with tarfile.open(tmp_crs_repo.path / 'pkg/test-pkg/1.2.43~1/pkg.tgz') as tf: + names = tf.getnames() + assert 'src/my-file.cpp' in names + assert 'include/my-header.hpp' in names + + +def test_repo_import2(bpt: BPTWrapper, tmp_crs_repo: CRSRepo) -> None: + tmp_crs_repo.import_(PROJECT_ROOT / 'data/simple2.crs', validate=False) + with tarfile.open(tmp_crs_repo.path / 'pkg/test-pkg/1.3.0~1/pkg.tgz') as tf: + names = tf.getnames() + assert 'include/my-header.hpp' in names + + +def test_repo_import3(bpt: BPTWrapper, tmp_crs_repo: CRSRepo) -> None: + tmp_crs_repo.import_(PROJECT_ROOT / 'data/simple3.crs', validate=False) + with tarfile.open(tmp_crs_repo.path / 'pkg/test-pkg/1.3.0~2/pkg.tgz') as tf: + names = tf.getnames() + assert 'src/my-file.cpp' in names + + +def test_repo_import4(bpt: BPTWrapper, tmp_crs_repo: CRSRepo) -> None: + tmp_crs_repo.import_(PROJECT_ROOT / 'data/simple4.crs', validate=False) + with tarfile.open(tmp_crs_repo.path / 'pkg/test-pkg/1.3.0~3/pkg.tgz') as tf: + names = tf.getnames() + assert 'src/deeper/my-file.cpp' in names + + +def test_repo_import_invalid_crs(bpt: BPTWrapper, tmp_crs_repo: CRSRepo, tmp_project: Project) -> None: + tmp_project.write('pkg.json', json.dumps({})) + with expect_error_marker('repo-import-invalid-crs-json'): + tmp_crs_repo.import_(tmp_project.root) + + +def test_repo_import_invalid_json(bpt: BPTWrapper, tmp_crs_repo: CRSRepo, tmp_project: Project) -> None: + tmp_project.write('pkg.json', 'not-json') + with expect_error_marker('repo-import-invalid-crs-json-parse-error'): + tmp_crs_repo.import_(tmp_project.root) + + +def test_repo_import_invalid_nodir(bpt: BPTWrapper, tmp_crs_repo: CRSRepo, tmp_path: Path) -> None: + with expect_error_marker('repo-import-noent'): + tmp_crs_repo.import_(tmp_path) + + +def test_repo_import_invalid_no_repo(bpt: BPTWrapper, tmp_path: Path, tmp_project: Project) -> None: + tmp_project.write('pkg.json', json.dumps({})) + with expect_error_marker('repo-repo-open-fails'): + bpt.run(['repo', 'import', tmp_path, tmp_project.root]) + + +def test_repo_import_db_too_new(bpt: BPTWrapper, tmp_path: Path, tmp_project: Project) -> None: + conn = sqlite3.connect(str(tmp_path / 'repo.db')) + conn.executescript(r''' + CREATE TABLE crs_repo_meta (version); + INSERT INTO crs_repo_meta (version) VALUES (300); + ''') + with expect_error_marker('repo-db-too-new'): + bpt.run(['repo', 'import', tmp_path, tmp_project.root]) + + +def test_repo_import_db_invalid(bpt: BPTWrapper, tmp_path: Path, tmp_project: Project) -> None: + conn = sqlite3.connect(str(tmp_path / 'repo.db')) + conn.executescript(r''' + CREATE TABLE crs_repo_meta (version); + INSERT INTO crs_repo_meta (version) VALUES ('eggs'); + ''') + with expect_error_marker('repo-db-invalid'): + bpt.run(['repo', 'import', tmp_path, tmp_project.root]) + + +def test_repo_import_db_invalid2(bpt: BPTWrapper, tmp_path: Path, tmp_project: Project) -> None: + tmp_path.joinpath('repo.db').write_bytes(b'not-a-sqlite3-database') + with expect_error_marker('repo-db-invalid'): + bpt.run(['repo', 'import', tmp_path, tmp_project.root]) + + +def test_repo_import_db_invalid3(bpt: BPTWrapper, tmp_path: Path, tmp_project: Project) -> None: + conn = sqlite3.connect(str(tmp_path / 'repo.db')) + conn.executescript(r''' + CREATE TABLE crs_repo_meta (version); + INSERT INTO crs_repo_meta (version) VALUES (1); + ''') + with expect_error_marker('repo-import-db-error'): + bpt.run(['repo', 'import', tmp_path, PROJECT_ROOT / 'data/simple.crs']) + + +@pytest.fixture(scope='session') +def simple_repo_base(crs_repo_factory: CRSRepoFactory) -> CRSRepo: + repo = crs_repo_factory('simple') + names = ('simple.crs', 'simple2.crs', 'simple3.crs', 'simple4.crs') + simples = (PROJECT_ROOT / 'data' / name for name in names) + repo.import_(simples, validate=False) + return repo + + +@pytest.fixture() +def simple_repo(simple_repo_base: CRSRepo, clone_repo: RepoCloner) -> CRSRepo: + return clone_repo(simple_repo_base) + + +def test_repo_double_import(bpt: BPTWrapper, simple_repo: CRSRepo) -> None: + with expect_error_marker('repo-import-pkg-already-exists'): + simple_repo.import_(PROJECT_ROOT / 'data/simple.crs') + + +def test_repo_double_import_ignore(bpt: BPTWrapper, simple_repo: CRSRepo) -> None: + before_time = simple_repo.path.joinpath('pkg/test-pkg/1.2.43~1/pkg.tgz').stat().st_mtime + simple_repo.import_(PROJECT_ROOT / 'data/simple.crs', if_exists='ignore', validate=False) + after_time = simple_repo.path.joinpath('pkg/test-pkg/1.2.43~1/pkg.tgz').stat().st_mtime + assert before_time == after_time + + +def test_repo_double_import_replace(bpt: BPTWrapper, simple_repo: CRSRepo) -> None: + before_time = simple_repo.path.joinpath('pkg/test-pkg/1.2.43~1/pkg.tgz').stat().st_mtime + simple_repo.import_(PROJECT_ROOT / 'data/simple.crs', if_exists='replace', validate=False) + after_time = simple_repo.path.joinpath('pkg/test-pkg/1.2.43~1/pkg.tgz').stat().st_mtime + assert before_time < after_time + + +def test_repo_remove(simple_repo: CRSRepo) -> None: + assert simple_repo.path.joinpath('pkg/test-pkg/1.2.43~1').exists() + simple_repo.remove('test-pkg@1.2.43~1') + assert not simple_repo.path.joinpath('pkg/test-pkg/1.2.43~1').exists() + + +def test_repo_remove_twice(simple_repo: CRSRepo) -> None: + simple_repo.remove('test-pkg@1.2.43~1') + with expect_error_marker('pkg-remove-nonesuch'): + simple_repo.remove('test-pkg@1.2.43~1') + + +def test_repo_remove_ignore_missing(simple_repo: CRSRepo) -> None: + simple_repo.remove('test-pkg@1.2.43~1') + simple_repo.remove('test-pkg@1.2.43~1', if_missing='ignore') + with expect_error_marker('pkg-remove-nonesuch'): + simple_repo.remove('test-pkg@1.2.43~1', if_missing='fail') + + +def test_pkg_prefetch_http_url(bpt: BPTWrapper, simple_repo: CRSRepo, http_server_factory: HTTPServerFactory, + tmp_path: Path) -> None: + srv = http_server_factory(simple_repo.path) + bpt.crs_cache_dir = tmp_path + bpt.pkg_prefetch(repos=[srv.base_url], pkgs=['test-pkg@1.2.43']) + assert tmp_path.joinpath('pkgs/test-pkg@1.2.43~1/pkg.json').is_file() + + +def test_pkg_prefetch_file_url(bpt: BPTWrapper, tmp_path: Path, simple_repo: CRSRepo) -> None: + bpt.crs_cache_dir = tmp_path + bpt.pkg_prefetch(repos=[str(simple_repo.path)], pkgs=['test-pkg@1.2.43']) + assert tmp_path.joinpath('pkgs/test-pkg@1.2.43~1/pkg.json').is_file() + + +def test_pkg_prefetch_404(bpt: BPTWrapper, tmp_path: Path, http_server_factory: HTTPServerFactory) -> None: + srv = http_server_factory(tmp_path) + bpt.crs_cache_dir = tmp_path + with expect_error_marker('repo-sync-http-404'): + bpt.pkg_prefetch(repos=[srv.base_url]) + + +def test_pkg_prefetch_invalid_tgz(bpt: BPTWrapper, tmp_path: Path, http_server_factory: HTTPServerFactory) -> None: + tmp_path.joinpath('repo.db.gz').write_text('lolhi') + srv = http_server_factory(tmp_path) + bpt.crs_cache_dir = tmp_path + with expect_error_marker('repo-sync-invalid-db-gz'): + bpt.pkg_prefetch(repos=[srv.base_url]) + + +def test_repo_validate_empty(tmp_crs_repo: CRSRepo) -> None: + tmp_crs_repo.validate() + + +def test_repo_validate_simple(tmp_crs_repo: CRSRepo, tmp_path: Path) -> None: + tmp_path.joinpath('pkg.json').write_text( + json.dumps({ + 'name': + 'foo', + 'version': + '1.2.3', + 'pkg-version': + 1, + 'schema-version': + 0, + 'libraries': [{ + 'path': '.', + 'name': 'foo', + 'using': [], + 'test-using': [], + 'dependencies': [], + 'test-dependencies': [], + }], + })) + tmp_crs_repo.import_(tmp_path) + tmp_crs_repo.validate() + + +def test_repo_validate_interdep(tmp_crs_repo: CRSRepo, tmp_path: Path) -> None: + # yapf: disable + tmp_path.joinpath('pkg.json').write_text( + json.dumps({ + 'name': 'foo', + 'version': '1.2.3', + 'pkg-version': 1, + 'schema-version': 0, + 'libraries': [{ + 'path': '.', + 'name': 'foo', + 'test-using': [], + 'using': ['bar'], + 'dependencies': [], + 'test-dependencies': [], + }, { + 'path': 'bar', + 'name': 'bar', + 'using': [], + 'test-using': [], + 'dependencies': [], + 'test-dependencies': [], + }], + })) + # yapf: enable + tmp_crs_repo.import_(tmp_path) + tmp_crs_repo.validate() + + +def test_repo_validate_invalid_no_sibling(tmp_crs_repo: CRSRepo, tmp_project: Project) -> None: + tmp_project.bpt_yaml = { + 'name': 'foo', + 'version': '1.2.3', + 'libraries': [{ + 'path': '.', + 'name': 'foo', + 'using': ['bar'], + }], + } + with expect_error_marker('repo-import-invalid-proj-json'): + tmp_crs_repo.import_(tmp_project.root) + + +def test_repo_invalid_pkg_version_zero(tmp_crs_repo: CRSRepo, tmp_path: Path) -> None: + tmp_path.joinpath('pkg.json').write_text(json.dumps(make_simple_crs('foo', '1.2.3', pkg_version=0))) + with expect_error_marker('repo-import-invalid-pkg-version'): + tmp_crs_repo.import_(tmp_path) + + +def test_repo_no_use_invalid_pkg_version(tmp_crs_repo: CRSRepo, tmp_project: Project) -> None: + ''' + Check that BPT refuses to acknowledge remote packages that have an invalid (<1) pkg revision. + + The 'repo import' utility will refuse to import them, but a hostile server could still + serve them. + ''' + # Replace the repo db with our own + db_path = tmp_crs_repo.path / 'repo.db' + db_path.unlink() + db = sqlite3.connect(str(db_path)) + db.executescript(r''' + CREATE TABLE crs_repo_self(rowid INTEGER PRIMARY KEY, name TEXT NOT NULL); + INSERT INTO crs_repo_self VALUES(1729, 'test'); + CREATE TABLE crs_repo_packages( + package_id INTEGER PRIMARY KEY, + meta_json TEXT NOT NULL + ); + ''') + with db: + db.execute(r'INSERT INTO crs_repo_packages(meta_json) VALUES(?)', + [json.dumps(make_simple_crs('bar', '1.2.3', pkg_version=1))]) + + tmp_project.bpt_yaml = { + 'name': 'foo', + 'version': '1.2.3', + 'dependencies': ['bar@1.2.3'], + } + tmp_project.bpt.run(['pkg', 'solve', '-r', tmp_crs_repo.path, 'bar@1.2.3']) + # Replace with a bad pkg_version: + with db: + db.execute('UPDATE crs_repo_packages SET meta_json=?', + [json.dumps(make_simple_crs('bar', '1.2.3', pkg_version=0))]) + with expect_error_marker('no-dependency-solution'): + tmp_project.bpt.run(['pkg', 'solve', '-r', tmp_crs_repo.path, 'bar@1.2.3']) diff --git a/tests/test_repoman.py b/tests/test_repoman.py deleted file mode 100644 index e4959fa1..00000000 --- a/tests/test_repoman.py +++ /dev/null @@ -1,56 +0,0 @@ -import pytest - -from dds_ci.dds import DDSWrapper -from dds_ci.testing.fixtures import Project -from dds_ci.testing.http import RepoServer -from dds_ci.testing.error import expect_error_marker -from pathlib import Path - - -@pytest.fixture() -def tmp_repo(tmp_path: Path, dds: DDSWrapper) -> Path: - dds.run(['repoman', 'init', tmp_path]) - return tmp_path - - -def test_add_simple(dds: DDSWrapper, tmp_repo: Path) -> None: - dds.run(['repoman', 'add', tmp_repo, 'git+https://github.com/vector-of-bool/neo-fun.git#0.6.0']) - with expect_error_marker('dup-pkg-add'): - dds.run(['repoman', 'add', tmp_repo, 'git+https://github.com/vector-of-bool/neo-fun.git#0.6.0']) - - -def test_add_github(dds: DDSWrapper, tmp_repo: Path) -> None: - dds.run(['repoman', 'add', tmp_repo, 'github:vector-of-bool/neo-fun/0.6.0']) - with expect_error_marker('dup-pkg-add'): - dds.run(['repoman', 'add', tmp_repo, 'github:vector-of-bool/neo-fun/0.6.0']) - - -def test_add_invalid(dds: DDSWrapper, tmp_repo: Path) -> None: - with expect_error_marker('repoman-add-invalid-pkg-url'): - dds.run(['repoman', 'add', tmp_repo, 'invalid://google.com/lolwut']) - - -def test_error_double_remove(tmp_repo: Path, dds: DDSWrapper) -> None: - dds.run([ - 'repoman', '-ltrace', 'add', tmp_repo, - 'https://github.com/vector-of-bool/neo-fun/archive/0.4.0.tar.gz?__dds_strpcmp=1' - ]) - dds.run(['repoman', 'remove', tmp_repo, 'neo-fun@0.4.0']) - - with expect_error_marker('repoman-rm-no-such-package'): - dds.run(['repoman', 'remove', tmp_repo, 'neo-fun@0.4.0']) - - -def test_pkg_http(http_repo: RepoServer, tmp_project: Project) -> None: - tmp_project.dds.run([ - 'repoman', '-ltrace', 'add', http_repo.server.root, - 'https://github.com/vector-of-bool/neo-fun/archive/0.4.0.tar.gz?__dds_strpcmp=1' - ]) - tmp_project.dds.repo_add(http_repo.url) - tmp_project.package_json = { - 'name': 'test', - 'version': '1.2.3', - 'depends': ['neo-fun@0.4.0'], - 'namespace': 'test', - } - tmp_project.build() diff --git a/tests/test_tweaks.py b/tests/test_tweaks.py index 2ddab016..45eae094 100644 --- a/tests/test_tweaks.py +++ b/tests/test_tweaks.py @@ -1,5 +1,5 @@ -from dds_ci.testing.fixtures import ProjectOpener -from dds_ci import paths, proc +from bpt_ci.testing.fixtures import ProjectOpener +from bpt_ci import paths, proc def test_lib_with_tweaks(project_opener: ProjectOpener) -> None: diff --git a/tests/test_uses/dependents/basic_dependent/bpt.yaml b/tests/test_uses/dependents/basic_dependent/bpt.yaml new file mode 100644 index 00000000..e3727084 --- /dev/null +++ b/tests/test_uses/dependents/basic_dependent/bpt.yaml @@ -0,0 +1,5 @@ +{ + name: 'test', + version: '0.0.0', + dependencies: ['the_test_lib@0.0.0 using the_test_lib'] +} diff --git a/tests/test_uses/dependents/basic_test_dependent/bpt.yaml b/tests/test_uses/dependents/basic_test_dependent/bpt.yaml new file mode 100644 index 00000000..c6671253 --- /dev/null +++ b/tests/test_uses/dependents/basic_test_dependent/bpt.yaml @@ -0,0 +1,3 @@ +name: "test" +version: "0.0.0" +test-dependencies: ["the_test_lib@0.0.0 using the_test_lib"] diff --git a/tests/test_uses/dependents/dependent_has_dep.test.cpp b/tests/test_uses/dependents/dependent_has_dep.test.cpp new file mode 100644 index 00000000..ce205d8f --- /dev/null +++ b/tests/test_uses/dependents/dependent_has_dep.test.cpp @@ -0,0 +1,12 @@ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include + +bool dependent_has_the_dep(); +bool dependent_has_direct_dep(); + +int main() { + assert(!::dependent_has_the_dep() && "Leaked test_depends to final dependent"); + assert(::dependent_has_direct_dep() && "Despite uses, no access to the_test_lib"); +} diff --git a/tests/test_uses/dependents/dependent_no_dep.test.cpp b/tests/test_uses/dependents/dependent_no_dep.test.cpp new file mode 100644 index 00000000..bf6aef5a --- /dev/null +++ b/tests/test_uses/dependents/dependent_no_dep.test.cpp @@ -0,0 +1,12 @@ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include + +bool dependent_has_the_dep(); +bool dependent_has_direct_dep(); + +int main() { + assert(!::dependent_has_the_dep() && "Leaked test_depends to final dependent"); + assert(!::dependent_has_direct_dep() && "Access to the_test_lib despite test_uses"); +} diff --git a/tests/test_uses/dependents/multi_lib_dependent/bpt.yaml b/tests/test_uses/dependents/multi_lib_dependent/bpt.yaml new file mode 100644 index 00000000..27a55fed --- /dev/null +++ b/tests/test_uses/dependents/multi_lib_dependent/bpt.yaml @@ -0,0 +1,9 @@ +name: "test" +version: "0.0.0" +libraries: + - path: "libs/uses" + name: "uses" + dependencies: ["the_test_lib@0.0.0"] + - path: "libs/uses_for_tests" + name: "uses_for_tests" + test-dependencies: ["the_test_lib@0.0.0 using the_test_lib"] diff --git a/tests/test_uses/dependents/src/dependent.cpp b/tests/test_uses/dependents/src/dependent.cpp new file mode 100644 index 00000000..ee6b9b3b --- /dev/null +++ b/tests/test_uses/dependents/src/dependent.cpp @@ -0,0 +1,15 @@ +bool dependent_has_the_dep() { +#if __has_include() + return true; +#else + return false; +#endif +} + +bool dependent_has_direct_dep() { +#if __has_include() + return true; +#else + return false; +#endif +} diff --git a/tests/test_uses/dependents/src/dependent.test.cpp b/tests/test_uses/dependents/src/dependent.test.cpp new file mode 100644 index 00000000..3f20f1ca --- /dev/null +++ b/tests/test_uses/dependents/src/dependent.test.cpp @@ -0,0 +1,11 @@ +#include "the_test_lib_d1df7125.hpp" + +#ifdef NDEBUG +#undef NDEBUG +#endif +#include + +int main() { + assert(!::has_the_dep() && "Serious error; the_test_lib is improperly set up"); + assert(!HAS_THE_DEP && "Leaked test_depends to final dependent"); +} diff --git a/tests/test_uses/dependents/test_depends_but_regular_uses/bpt.yaml b/tests/test_uses/dependents/test_depends_but_regular_uses/bpt.yaml new file mode 100644 index 00000000..63760292 --- /dev/null +++ b/tests/test_uses/dependents/test_depends_but_regular_uses/bpt.yaml @@ -0,0 +1,3 @@ +name: "test" +version: "0.0.0" +dependencies: [{ dep: "the_test_lib@0.0.0", for: "test" }] diff --git a/tests/test_uses/partially_buildable/bpt.yaml b/tests/test_uses/partially_buildable/bpt.yaml new file mode 100644 index 00000000..c1ef9442 --- /dev/null +++ b/tests/test_uses/partially_buildable/bpt.yaml @@ -0,0 +1,7 @@ +name: partially_buildable +version: 0.1.0 +libraries: + - path: can_build + name: can_build + - path: cannot_build + name: cannot_build diff --git a/tests/test_uses/partially_buildable/can_build/src/can_build/header.hpp b/tests/test_uses/partially_buildable/can_build/src/can_build/header.hpp new file mode 100644 index 00000000..2a66be3a --- /dev/null +++ b/tests/test_uses/partially_buildable/can_build/src/can_build/header.hpp @@ -0,0 +1,5 @@ +#pragma once + +// Just an empty header + +constexpr int CAN_BUILD_ZERO = 0; \ No newline at end of file diff --git a/tests/test_uses/partially_buildable/can_build/src/can_build/source.cpp b/tests/test_uses/partially_buildable/can_build/src/can_build/source.cpp new file mode 100644 index 00000000..ed2cfd77 --- /dev/null +++ b/tests/test_uses/partially_buildable/can_build/src/can_build/source.cpp @@ -0,0 +1,3 @@ +#include "./header.hpp" + +static_assert(CAN_BUILD_ZERO == 0, "Check that zero is zero"); diff --git a/tests/test_uses/partially_buildable/cannot_build/src/fail.cpp b/tests/test_uses/partially_buildable/cannot_build/src/fail.cpp new file mode 100644 index 00000000..bd8f801a --- /dev/null +++ b/tests/test_uses/partially_buildable/cannot_build/src/fail.cpp @@ -0,0 +1 @@ +#error "This file will never compile" \ No newline at end of file diff --git a/tests/test_uses/test_uses_test.py b/tests/test_uses/test_uses_test.py new file mode 100644 index 00000000..6a530eac --- /dev/null +++ b/tests/test_uses/test_uses_test.py @@ -0,0 +1,147 @@ +import pytest +import shutil +from pathlib import Path + +from bpt_ci.testing import ProjectOpener, CRSRepo, CRSRepoFactory, error, fs +from bpt_ci.testing.fixtures import Project + + +@pytest.fixture(scope='module') +def ut_repo(crs_repo_factory: CRSRepoFactory, test_parent_dir: Path) -> CRSRepo: + repo = crs_repo_factory('uses-test') + names = ('the_test_dependency', 'the_test_lib', 'unbuildable', 'with_bad_test_dep', 'partially_buildable') + repo.import_((test_parent_dir / name for name in names)) + return repo + + +def test_build_lib_with_failing_test_dep(project_opener: ProjectOpener, ut_repo: CRSRepo) -> None: + proj = project_opener.open('with_bad_test_dep') + # If we disable tests, then we won't try to build the test libraries, and + # therefore won't hit the compilation error + proj.build(with_tests=False, repos=[ut_repo.path]) + # Compiling with tests enabled produces an error + with error.expect_error_marker('compile-failed'): + proj.build(with_tests=True, repos=[ut_repo.path]) + # Check that nothing changed spuriously + proj.build(with_tests=False, repos=[ut_repo.path]) + + +def test_build_lib_with_failing_transitive_test_dep(project_opener: ProjectOpener, ut_repo: CRSRepo) -> None: + proj = project_opener.open('with_transitive_bad_test_dep') + # Even though the transitive dependency has a test-dependency that fails, we + # won't ever try to build that dependency ourselves, so we're okay. + proj.build(with_tests=True, repos=[ut_repo.path]) + proj.build(with_tests=False, repos=[ut_repo.path]) + proj.build(with_tests=True, repos=[ut_repo.path]) + proj.build(with_tests=False, repos=[ut_repo.path]) + + +def test_build_lib_with_test_uses(project_opener: ProjectOpener, ut_repo: CRSRepo) -> None: + proj = project_opener.open('the_test_lib') + proj.build(repos=[ut_repo.path]) + + +def test_build_dependent_of_lib_with_test_uses(test_parent_dir: Path, project_opener: ProjectOpener, + ut_repo: CRSRepo) -> None: + proj = project_opener.open('dependents/basic_dependent') + shutil.copytree(test_parent_dir / 'dependents/src', proj.root / 'src') + shutil.copy(test_parent_dir / 'dependents/dependent_has_dep.test.cpp', proj.root / 'src') + proj.build(repos=[ut_repo.path]) + + +def test_build_test_dependent_of_lib_with_test_uses(test_parent_dir: Path, project_opener: ProjectOpener, + ut_repo: CRSRepo) -> None: + proj = project_opener.open('dependents/basic_test_dependent') + shutil.copytree(test_parent_dir / 'dependents/src', proj.root / 'src') + shutil.copy(test_parent_dir / 'dependents/dependent_no_dep.test.cpp', proj.root / 'src') + proj.build(repos=[ut_repo.path]) + + +def test_build_dependent_with_multiple_libs(test_parent_dir: Path, project_opener: ProjectOpener, + ut_repo: CRSRepo) -> None: + proj = project_opener.open('dependents/multi_lib_dependent') + shutil.copytree(test_parent_dir / 'dependents/src', proj.root / 'libs/uses/src/uses') + shutil.copy(test_parent_dir / 'dependents/dependent_has_dep.test.cpp', proj.root / 'libs/uses/src') + shutil.copytree(test_parent_dir / 'dependents/src', proj.root / 'libs/uses_for_tests/src/test_uses') + shutil.copy(test_parent_dir / 'dependents/dependent_no_dep.test.cpp', proj.root / 'libs/uses_for_tests/src') + proj.build(repos=[ut_repo.path]) + + +def test_uses_sibling_lib(tmp_project: Project) -> None: + fs.render_into( + tmp_project.root, { + 'main': { + 'src': { + 'foo.cpp': b'int number() { return 42; }\n', + 'foo.h': 'extern int number();\n', + } + }, + 'there': { + 'src': { + 't.test.cpp': + r''' + #include + #include + + int main () { assert(number() == 42); } + ''' + } + } + }) + + # Missing the 'using' + tmp_project.bpt_yaml = { + 'name': 'testing', + 'version': '1.2.3', + 'libraries': [{ + 'name': 'main', + 'path': 'main', + }, { + 'name': 'other', + 'path': 'there', + }] + } + with error.expect_error_marker('compile-failed'): + tmp_project.build() + + # Now add the missing 'using' + tmp_project.bpt_yaml = { + 'name': 'testing', + 'version': '1.2.3', + 'libraries': [{ + 'name': 'main', + 'path': 'main', + }, { + 'name': 'other', + 'path': 'there', + 'using': ['main'] + }] + } + tmp_project.build() + + +def test_build_partial_dep(tmp_project: Project, ut_repo: CRSRepo) -> None: + fs.render_into( + tmp_project.root, { + 'src': { + 'use-lib.cpp': + r''' + #include + + int main() { + return CAN_BUILD_ZERO; + } + ''', + }, + }) + + tmp_project.bpt_yaml = { + 'name': 'test-user', + 'version': '1.2.3', + 'dependencies': ['partially_buildable@0.1.0 using can_build'], + } + tmp_project.build(repos=[ut_repo.path]) + + tmp_project.bpt_yaml['dependencies'] = ['partially_buildable@0.1.0 using can_build, cannot_build'] + with error.expect_error_marker('compile-failed'): + tmp_project.build(repos=[ut_repo.path]) diff --git a/tests/test_uses/the_test_dependency/bpt.yaml b/tests/test_uses/the_test_dependency/bpt.yaml new file mode 100644 index 00000000..bc195b0d --- /dev/null +++ b/tests/test_uses/the_test_dependency/bpt.yaml @@ -0,0 +1,10 @@ +{ + name: "the_test_dependency", + version: "0.0.0", + + libraries: + [ + { path: ., name: "the_test_dependency", using: [sibling] }, + { path: sibling, name: sibling }, + ], +} diff --git a/tests/test_uses/the_test_dependency/include/the_test_dependency_24fe5647.hpp b/tests/test_uses/the_test_dependency/include/the_test_dependency_24fe5647.hpp new file mode 100644 index 00000000..5f8f4a58 --- /dev/null +++ b/tests/test_uses/the_test_dependency/include/the_test_dependency_24fe5647.hpp @@ -0,0 +1,7 @@ +#pragma once + +#include + +namespace the_test_dependency { +inline bool some_fn() { return true; } +} // namespace the_test_dependency diff --git a/tests/test_uses/the_test_dependency/sibling/src/test-sibling.h b/tests/test_uses/the_test_dependency/sibling/src/test-sibling.h new file mode 100644 index 00000000..fefa00ad --- /dev/null +++ b/tests/test_uses/the_test_dependency/sibling/src/test-sibling.h @@ -0,0 +1 @@ +// This file is intentionally left blank \ No newline at end of file diff --git a/tests/test_uses/the_test_lib/bpt.yaml b/tests/test_uses/the_test_lib/bpt.yaml new file mode 100644 index 00000000..7835d407 --- /dev/null +++ b/tests/test_uses/the_test_lib/bpt.yaml @@ -0,0 +1,3 @@ +name: "the_test_lib" +version: "0.0.0" +test-dependencies: ["the_test_dependency@0.0.0 using the_test_dependency"] diff --git a/tests/test_uses/the_test_lib/include/the_test_lib_d1df7125.hpp b/tests/test_uses/the_test_lib/include/the_test_lib_d1df7125.hpp new file mode 100644 index 00000000..075b4992 --- /dev/null +++ b/tests/test_uses/the_test_lib/include/the_test_lib_d1df7125.hpp @@ -0,0 +1,13 @@ +#pragma once + +#ifdef __has_include +#if __has_include() +#define HAS_THE_DEP true +#endif +#endif + +#ifndef HAS_THE_DEP +#define HAS_THE_DEP false +#endif + +bool has_the_dep(); diff --git a/tests/test_uses/the_test_lib/src/the_test_lib_d1df7125.cpp b/tests/test_uses/the_test_lib/src/the_test_lib_d1df7125.cpp new file mode 100644 index 00000000..cba84a34 --- /dev/null +++ b/tests/test_uses/the_test_lib/src/the_test_lib_d1df7125.cpp @@ -0,0 +1,3 @@ +#include "the_test_lib_d1df7125.hpp" + +bool has_the_dep() { return HAS_THE_DEP; } diff --git a/tests/test_uses/the_test_lib/src/xyz.test.cpp b/tests/test_uses/the_test_lib/src/xyz.test.cpp new file mode 100644 index 00000000..a6853097 --- /dev/null +++ b/tests/test_uses/the_test_lib/src/xyz.test.cpp @@ -0,0 +1,12 @@ +#include "the_test_lib_d1df7125.hpp" + +#ifdef NDEBUG +#undef NDEBUG +#endif + +#include + +int main() { + assert(HAS_THE_DEP); + assert(!::has_the_dep()); +} diff --git a/tests/test_uses/unbuildable/bpt.yaml b/tests/test_uses/unbuildable/bpt.yaml new file mode 100644 index 00000000..6f1e1dcf --- /dev/null +++ b/tests/test_uses/unbuildable/bpt.yaml @@ -0,0 +1,5 @@ +{ + name: "unbuildable", + version: "0.0.0", + libraries: [{ name: "fails_to_build", path: . }], +} diff --git a/tests/test_uses/unbuildable/src/bad.cpp b/tests/test_uses/unbuildable/src/bad.cpp new file mode 100644 index 00000000..d9be5dca --- /dev/null +++ b/tests/test_uses/unbuildable/src/bad.cpp @@ -0,0 +1 @@ +This file will not compile \ No newline at end of file diff --git a/tests/test_uses/with_bad_test_dep/bpt.yaml b/tests/test_uses/with_bad_test_dep/bpt.yaml new file mode 100644 index 00000000..ff0e0784 --- /dev/null +++ b/tests/test_uses/with_bad_test_dep/bpt.yaml @@ -0,0 +1,10 @@ +# prettier-ignore +{ + "name": "with_bad_test_dep", + "version": "0.0.0", + "test-dependencies": [ + # This dependency cannot compile. We will therefore fail to build + # if we build with tests + "unbuildable@0.0.0 using fails_to_build" + ] +} diff --git a/tools/dds_ci/__init__.py b/tests/test_uses/with_bad_test_dep/src/thing.cpp similarity index 100% rename from tools/dds_ci/__init__.py rename to tests/test_uses/with_bad_test_dep/src/thing.cpp diff --git a/tests/test_uses/with_transitive_bad_test_dep/bpt.yaml b/tests/test_uses/with_transitive_bad_test_dep/bpt.yaml new file mode 100644 index 00000000..411b1d80 --- /dev/null +++ b/tests/test_uses/with_transitive_bad_test_dep/bpt.yaml @@ -0,0 +1,13 @@ +# prettier-ignore +{ + "name": "uses-bad-dep", + "version": "0.0.0", + "dependencies": [ + { + # This dependency's test dependency cannot compile. We will therefore + # fail to build if we try to build its tests. Since BPT should not try + # to build its test dependencies, we will have no issue here + "dep": "with_bad_test_dep@0.0.0", + } + ] +} diff --git a/tests/test_uses/with_transitive_bad_test_dep/src/thing.cpp b/tests/test_uses/with_transitive_bad_test_dep/src/thing.cpp new file mode 100644 index 00000000..e69de29b diff --git a/tests/use-cryptopp/gcc.tc.jsonc b/tests/use-cryptopp/gcc.tc.jsonc deleted file mode 100644 index a7392fe2..00000000 --- a/tests/use-cryptopp/gcc.tc.jsonc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "compiler_id": "gnu", - "cxx_compiler": "g++-9", - "cxx_version": "c++17", - // All required for Crypto++ intrinsics: - "flags": "-msse2 -msse3 -mssse3 -msse4.1 -msse4.2 -mpclmul -maes -mavx -mavx2 -msha -Wa,-q -DCRYPTOPP_DISABLE_ASM=1", -} \ No newline at end of file diff --git a/tests/use-cryptopp/msvc.tc.jsonc b/tests/use-cryptopp/msvc.tc.jsonc deleted file mode 100644 index 92fc7fd1..00000000 --- a/tests/use-cryptopp/msvc.tc.jsonc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "compiler_id": 'msvc', - "flags": "/std:c++17 /DCRYPTOPP_DISABLE_ASM=1" -} \ No newline at end of file diff --git a/tests/use-cryptopp/test_use_cryptopp.py b/tests/use-cryptopp/test_use_cryptopp.py deleted file mode 100644 index eb2e3aab..00000000 --- a/tests/use-cryptopp/test_use_cryptopp.py +++ /dev/null @@ -1,70 +0,0 @@ -from pathlib import Path -import platform - -import pytest - -from dds_ci.testing import RepoServer, Project -from dds_ci import proc, toolchain, paths - -CRYPTOPP_JSON = { - "packages": { - "cryptopp": { - "8.2.0": { - "remote": { - "git": { - "url": "https://github.com/weidai11/cryptopp.git", - "ref": "CRYPTOPP_8_2_0" - }, - "auto-lib": "cryptopp/cryptopp", - "transform": [{ - "move": { - "from": ".", - "to": "src/cryptopp", - "include": ["*.c", "*.cpp", "*.h"] - } - }] - } - } - } - } -} - -APP_CPP = r''' -#include - -#include - -int main() { - std::string arr; - arr.resize(256); - CryptoPP::OS_GenerateRandomBlock(false, - reinterpret_cast(arr.data()), - arr.size()); - for (auto b : arr) { - if (b != '\x00') { - return 0; - } - } - return 1; -} -''' - - -@pytest.mark.skipif(platform.system() == 'FreeBSD', reason='This one has trouble running on FreeBSD') -def test_get_build_use_cryptopp(test_parent_dir: Path, tmp_project: Project, http_repo: RepoServer) -> None: - http_repo.import_json_data(CRYPTOPP_JSON) - tmp_project.dds.repo_add(http_repo.url) - tmp_project.package_json = { - 'name': 'usr-cryptopp', - 'version': '1.0.0', - 'namespace': 'test', - 'depends': ['cryptopp@8.2.0'], - } - tmp_project.library_json = { - 'name': 'use-cryptopp', - 'uses': ['cryptopp/cryptopp'], - } - tc_fname = 'gcc.tc.jsonc' if 'gcc' in toolchain.get_default_test_toolchain().name else 'msvc.tc.jsonc' - tmp_project.write('src/use-cryptopp.main.cpp', APP_CPP) - tmp_project.build(toolchain=test_parent_dir / tc_fname, timeout = 60*10) - proc.check_run([(tmp_project.build_root / 'use-cryptopp').with_suffix(paths.EXE_SUFFIX)]) diff --git a/tests/use-spdlog/gcc.tc.jsonc b/tests/use-spdlog/gcc.tc.jsonc index a7309cd2..5caecefc 100644 --- a/tests/use-spdlog/gcc.tc.jsonc +++ b/tests/use-spdlog/gcc.tc.jsonc @@ -1,6 +1,6 @@ { "compiler_id": 'gnu', "cxx_version": 'c++17', - "cxx_compiler": 'g++-9', + "cxx_compiler": 'g++-10', "flags": '-DSPDLOG_COMPILED_LIB', } \ No newline at end of file diff --git a/tests/use-spdlog/project/bpt.yaml b/tests/use-spdlog/project/bpt.yaml new file mode 100644 index 00000000..6efb50c7 --- /dev/null +++ b/tests/use-spdlog/project/bpt.yaml @@ -0,0 +1,5 @@ +{ + "name": "test", + "version": "0.0.0", + "dependencies": ["spdlog@1.4.2 using spdlog"], +} diff --git a/tests/use-spdlog/project/catalog.json b/tests/use-spdlog/project/catalog.json deleted file mode 100644 index 33df55de..00000000 --- a/tests/use-spdlog/project/catalog.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "version": 2, - "packages": { - "spdlog": { - "1.4.2": { - "remote": { - "git": { - "url": "https://github.com/gabime/spdlog.git", - "ref": "v1.4.2" - }, - "auto-lib": "spdlog/spdlog" - } - } - } - } -} \ No newline at end of file diff --git a/tests/use-spdlog/project/library.json5 b/tests/use-spdlog/project/library.json5 deleted file mode 100644 index b8699e2c..00000000 --- a/tests/use-spdlog/project/library.json5 +++ /dev/null @@ -1,6 +0,0 @@ -{ - name: 'spdlog-user', - uses: [ - 'spdlog/spdlog', - ] -} \ No newline at end of file diff --git a/tests/use-spdlog/project/package.json5 b/tests/use-spdlog/project/package.json5 deleted file mode 100644 index 3e28942d..00000000 --- a/tests/use-spdlog/project/package.json5 +++ /dev/null @@ -1,8 +0,0 @@ -{ - name: 'test', - version: '0.0.0', - "namespace": "test", - depends: [ - 'spdlog@1.4.2', - ], -} \ No newline at end of file diff --git a/tests/use-spdlog/project/src/use-spdlog.main.cpp b/tests/use-spdlog/project/src/use-spdlog.main.cpp index 73028f83..e49b9eed 100644 --- a/tests/use-spdlog/project/src/use-spdlog.main.cpp +++ b/tests/use-spdlog/project/src/use-spdlog.main.cpp @@ -6,7 +6,7 @@ int main() { auto result = ::write_message(); if (result != 42) { spdlog::critical( - "The test library returned the wrong value (This is a REAL dds test failure, and is " + "The test library returned the wrong value (This is a REAL bpt test failure, and is " "very unexpected)"); return 1; } diff --git a/tests/use-spdlog/use_spdlog_test.py b/tests/use-spdlog/use_spdlog_test.py index bd99f417..e6c43f1c 100644 --- a/tests/use-spdlog/use_spdlog_test.py +++ b/tests/use-spdlog/use_spdlog_test.py @@ -1,13 +1,50 @@ +import json +import pytest from pathlib import Path -from dds_ci.testing import RepoServer, ProjectOpener -from dds_ci import proc, paths, toolchain +from bpt_ci.testing import ProjectOpener, error, CRSRepo, CRSRepoFactory +from bpt_ci import proc, paths, toolchain +from bpt_ci.testing.fs import DirRenderer, render_into +from bpt_ci.testing.repo import make_simple_crs -def test_get_build_use_spdlog(test_parent_dir: Path, project_opener: ProjectOpener, http_repo: RepoServer) -> None: - proj = project_opener.open('project') - http_repo.import_json_file(proj.root / 'catalog.json') - proj.dds.repo_add(http_repo.url) +@pytest.fixture(scope='session') +def spdlog_v1_4_2(dir_renderer: DirRenderer) -> Path: + with dir_renderer.get_or_prepare('spdlog-v1.4.2-v1') as prep: + if prep.ready_path: + return prep.ready_path + assert prep.prep_path + proc.check_run( + ['git', 'clone', 'https://github.com/gabime/spdlog.git', '--branch=v1.4.2', '--depth=1', prep.prep_path]) + render_into(prep.prep_path, {'pkg.json': json.dumps(make_simple_crs('spdlog', '1.4.2'))}) + return prep.commit() + + +@pytest.fixture(scope='module') +def repo_with_spdlog(crs_repo_factory: CRSRepoFactory, spdlog_v1_4_2: Path) -> CRSRepo: + repo = crs_repo_factory('with-spdlog') + repo.import_(spdlog_v1_4_2) + return repo + + +@pytest.fixture() +def toolchain_path(test_parent_dir: Path) -> Path: tc_fname = 'gcc.tc.jsonc' if 'gcc' in toolchain.get_default_test_toolchain().name else 'msvc.tc.jsonc' - proj.build(toolchain=test_parent_dir / tc_fname) + return test_parent_dir / tc_fname + + +def test_get_build_use_spdlog(project_opener: ProjectOpener, toolchain_path: Path, repo_with_spdlog: CRSRepo) -> None: + proj = project_opener.open('project') + proj.build(toolchain=toolchain_path, repos=[repo_with_spdlog.path]) proc.check_run([(proj.build_root / 'use-spdlog').with_suffix(paths.EXE_SUFFIX)]) + + +def test_invalid_uses_specifier(project_opener: ProjectOpener, toolchain_path: Path, repo_with_spdlog: CRSRepo) -> None: + proj = project_opener.open('project') + proj.bpt_yaml = { + 'name': 'test', + 'version': '0.0.0', + 'dependencies': ['spdlog@1.4.2 using no-such-library'], + } + with error.expect_error_marker('no-dependency-solution'): + proj.build(toolchain=toolchain_path, repos=[repo_with_spdlog.path]) diff --git a/tools/Dockerfile.alpine b/tools/Dockerfile.alpine index f2785c73..41cb209e 100644 --- a/tools/Dockerfile.alpine +++ b/tools/Dockerfile.alpine @@ -1,21 +1,21 @@ -FROM alpine:3.12.1 +FROM alpine:3.15.4 # Base build dependencies -RUN apk add "gcc=9.3.0-r2" "g++=9.3.0-r2" make python3 py3-pip \ - git openssl-libs-static openssl-dev ccache lld curl python3-dev cmake +RUN apk add "gcc~10.3" "g++~10.3" make python3 py3-pip \ + git openssl-libs-static openssl-dev ccache lld curl python3-dev cmake clang # We use version-qualified names for compiler executables -RUN ln -s $(type -P gcc) /usr/local/bin/gcc-9 && \ - ln -s $(type -P g++) /usr/local/bin/g++-9 +RUN ln -s $(type -P gcc) /usr/local/bin/gcc-10 && \ + ln -s $(type -P g++) /usr/local/bin/g++-10 # We want the UID in the container to match the UID on the outside, for minimal # fuss with file permissions -ARG DDS_USER_UID=1000 +ARG BPT_USER_UID=1000 RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py \ | env POETRY_HOME=/opt/poetry python3 -u - --no-modify-path && \ ln -s /opt/poetry/bin/poetry /usr/local/bin/poetry && \ chmod a+x /opt/poetry/bin/poetry && \ - adduser --disabled-password --uid=${DDS_USER_UID} dds + adduser --disabled-password --uid=${BPT_USER_UID} bpt -USER dds +USER bpt diff --git a/tools/bpt_ci/__init__.py b/tools/bpt_ci/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/bpt_ci/bootstrap.py b/tools/bpt_ci/bootstrap.py new file mode 100644 index 00000000..e675d446 --- /dev/null +++ b/tools/bpt_ci/bootstrap.py @@ -0,0 +1,259 @@ +import enum +import os +import platform +import shutil +import sys +import urllib.request +from contextlib import contextmanager +from pathlib import Path +from typing import Iterator, Mapping, Optional + +from . import paths, proc +from .bpt import BPTWrapper +from .paths import new_tempdir + + +class BootstrapMode(enum.Enum): + """How should be bootstrap our prior BPT executable?""" + Download = 'download' + 'Downlaod one from GitHub' + Build = 'build' + 'Build one from source' + Skip = 'skip' + 'Skip bootstrapping. Assume it already exists.' + Lazy = 'lazy' + 'If the prior executable exists, skip, otherwise download' + + +def _do_bootstrap_download() -> Path: + filename = { + 'win32': 'dds-win-x64.exe', + 'linux': 'dds-linux-x64', + 'darwin': 'dds-macos-x64', + 'freebsd11': 'dds-freebsd-x64', + 'freebsd12': 'dds-freebsd-x64', + }.get(sys.platform) + if filename is None: + raise RuntimeError(f'We do not have a prebuilt DDS binary for the "{sys.platform}" platform') + url = f'https://github.com/vector-of-bool/dds/releases/download/0.1.0-alpha.6/{filename}' + + print(f'Downloading prebuilt BPT executable: {url}') + with urllib.request.urlopen(url) as stream: + paths.PREBUILT_BPT.parent.mkdir(exist_ok=True, parents=True) + with paths.PREBUILT_BPT.open('wb') as fd: + while True: + buf = stream.read(1024 * 4) + if not buf: + break + fd.write(buf) + + if sys.platform != 'win32': + # Mark the binary executable. By default it won't be + mode = paths.PREBUILT_BPT.stat().st_mode + mode |= 0b001_001_001 + paths.PREBUILT_BPT.chmod(mode) + + return paths.PREBUILT_BPT + + +@contextmanager +def pin_exe(fpath: Path) -> Iterator[Path]: + """ + Create a copy of the file at ``fpath`` at an unspecified location, and + yield that path. + + This is needed if the executable would overwrite itself. + """ + with new_tempdir() as tdir: + tfile = tdir / 'previous-bpt.exe' + shutil.copy2(fpath, tfile) + yield tfile + + +def get_bootstrap_exe(mode: BootstrapMode) -> BPTWrapper: + """Obtain a ``bpt`` executable for the given bootstrapping mode""" + if mode is BootstrapMode.Lazy: + f = paths.PREBUILT_BPT + if not f.exists(): + _do_bootstrap_download() + elif mode is BootstrapMode.Download: + f = _do_bootstrap_download() + elif mode is BootstrapMode.Build: + f = _do_bootstrap_build() + elif mode is BootstrapMode.Skip: + f = paths.PREBUILT_BPT + + return BPTWrapper(f) + + +def _do_bootstrap_build() -> Path: + return _bootstrap_p6() + + +def _bootstrap_p6() -> Path: + prev_bpt = _bootstrap_alpha_4() + p6_dir = paths.PREBUILT_DIR / 'p6' + ret_bpt = _bpt_in(p6_dir) + if ret_bpt.exists(): + return ret_bpt + + _clone_self_at(p6_dir, '0.1.0-alpha.6-bootstrap') + tc = 'msvc-rel.jsonc' if platform.system() == 'Windows' else 'gcc-10-rel.jsonc' + + catalog_arg = f'--catalog={p6_dir}/_catalog.db' + repo_arg = f'--repo-dir={p6_dir}/_repo' + + proc.check_run( + [prev_bpt, 'catalog', 'import', catalog_arg, '--json=old-catalog.json'], + cwd=p6_dir, + ) + proc.check_run( + [ + prev_bpt, + 'build', + catalog_arg, + repo_arg, + ('--toolchain', p6_dir / 'tools' / tc), + ], + cwd=p6_dir, + ) + return ret_bpt + + +def _bootstrap_alpha_4() -> Path: + prev_bpt = _bootstrap_alpha_3() + a4_dir = paths.PREBUILT_DIR / 'alpha-4' + ret_bpt = _bpt_in(a4_dir) + if ret_bpt.exists(): + return ret_bpt + + _clone_self_at(a4_dir, '0.1.0-alpha.4') + build_py = a4_dir / 'tools/build.py' + proc.check_run( + [ + sys.executable, + '-u', + build_py, + ], + env=_prev_bpt_env(prev_bpt), + cwd=a4_dir, + ) + return ret_bpt + + +def _bootstrap_alpha_3() -> Path: + prev_bpt = _bootstrap_p5() + a3_dir = paths.PREBUILT_DIR / 'alpha-3' + ret_bpt = _bpt_in(a3_dir) + if ret_bpt.exists(): + return ret_bpt + + _clone_self_at(a3_dir, '0.1.0-alpha.3') + build_py = a3_dir / 'tools/build.py' + proc.check_run( + [ + sys.executable, + '-u', + build_py, + ], + env=_prev_bpt_env(prev_bpt), + cwd=a3_dir, + ) + return ret_bpt + + +def _bootstrap_p5() -> Path: + prev_bpt = _bootstrap_p4() + p5_dir = paths.PREBUILT_DIR / 'p5' + ret_bpt = _bpt_in(p5_dir) + if ret_bpt.exists(): + return ret_bpt + + _clone_self_at(p5_dir, 'bootstrap-p5.2') + build_py = p5_dir / 'tools/build.py' + proc.check_run( + [ + sys.executable, + '-u', + build_py, + ], + env=_prev_bpt_env(prev_bpt), + cwd=p5_dir, + ) + return ret_bpt + + +def _bootstrap_p4() -> Path: + prev_bpt = _bootstrap_p1() + p4_dir = paths.PREBUILT_DIR / 'p4' + p4_dir.mkdir(exist_ok=True, parents=True) + ret_bpt = _bpt_in(p4_dir) + if ret_bpt.exists(): + return ret_bpt + + _clone_self_at(p4_dir, 'bootstrap-p4.2') + build_py = p4_dir / 'tools/build.py' + proc.check_run( + [ + sys.executable, + '-u', + build_py, + '--cxx=cl.exe' if platform.system() == 'Windows' else '--cxx=g++-8', + ], + env=_prev_bpt_env(prev_bpt), + ) + return ret_bpt + + +def _bootstrap_p1() -> Path: + p1_dir = paths.PREBUILT_DIR / 'p1' + p1_dir.mkdir(exist_ok=True, parents=True) + ret_bpt = _bpt_in(p1_dir) + if ret_bpt.exists(): + return ret_bpt + + _clone_self_at(p1_dir, 'bootstrap-p1.2') + build_py = p1_dir / 'tools/build.py' + proc.check_run([ + sys.executable, + '-u', + build_py, + '--cxx=cl.exe' if platform.system() == 'Windows' else '--cxx=g++-8', + ]) + _build_prev(p1_dir) + return ret_bpt + + +def _clone_self_at(dirpath: Path, ref: str) -> None: + if dirpath.is_dir(): + shutil.rmtree(dirpath) + dirpath.mkdir(exist_ok=True, parents=True) + proc.check_run(['git', 'clone', '-qq', paths.PROJECT_ROOT, f'--branch={ref}', dirpath]) + + +def _bpt_in(dirpath: Path) -> Path: + return dirpath.joinpath('_build/bpt' + paths.EXE_SUFFIX) + + +def _prev_bpt_env(bpt: Path) -> Mapping[str, str]: + env = os.environ.copy() + env.update({'BPT_BOOTSTRAP_PREV_EXE': str(bpt)}) + return env + + +def _build_prev(dirpath: Path, prev_bpt: Optional[Path] = None) -> None: + build_py = dirpath / 'tools/build.py' + env: Optional[Mapping[str, str]] = None + if prev_bpt is not None: + env = os.environ.copy() + env['BPT_BOOSTRAP_PREV_EXE'] = str(prev_bpt) + proc.check_run( + [ + sys.executable, + '-u', + build_py, + '--cxx=cl.exe' if platform.system() == 'Windows' else '--cxx=g++-8', + ], + cwd=dirpath, + env=env, + ) diff --git a/tools/bpt_ci/bpt.py b/tools/bpt_ci/bpt.py new file mode 100644 index 00000000..2a34ab0f --- /dev/null +++ b/tools/bpt_ci/bpt.py @@ -0,0 +1,168 @@ +from __future__ import annotations + +import copy +import multiprocessing +import os +import shutil +from pathlib import Path +from typing import Iterable, TypeVar + +from bpt_ci.util import Pathish + +from . import proc +from . import toolchain as tc_mod + +T = TypeVar('T') +""" +A generic unbounded type parameter +""" + + +class BPTWrapper: + """ + Wraps a 'bpt' executable with some convenience APIs that invoke various + 'bpt' subcommands. + """ + + def __init__(self, path: Path, *, crs_cache_dir: Pathish | None = None, default_cwd: Pathish | None = None) -> None: + self.path = path + "The path to the wrapped ``bpt`` executable" + self.default_cwd = Path(default_cwd or Path.cwd()) + "The directory in which ``bpt`` commands will execute unless otherwise specified" + self.crs_cache_dir = crs_cache_dir + "The directory in which the CRS cache data will be stored" + + def clone(self) -> BPTWrapper: + "Create a clone of this object" + return copy.deepcopy(self) + + @property + def always_args(self) -> proc.CommandLine: + """Arguments that are always given to every bpt subcommand""" + return [self.crs_cache_dir_arg] + + @property + def crs_cache_dir_arg(self) -> proc.CommandLine: + """The arguments for ``--crs-cache-dir``""" + if self.crs_cache_dir is not None: + return [f'--crs-cache-dir={self.crs_cache_dir}'] + return [] + + def clean(self, *, build_dir: Path | None = None, crs_cache: bool = True) -> None: + """ + Clean out prior executable output, including the CRS cache and + the build results at 'build_dir', if given. + """ + if build_dir and build_dir.exists(): + shutil.rmtree(build_dir) + if crs_cache and self.crs_cache_dir: + p = Path(self.crs_cache_dir) + if p.exists(): + shutil.rmtree(p) + + def run(self, args: proc.CommandLine, *, cwd: Pathish | None = None, timeout: float | None = None) -> None: + """ + Execute the 'bpt' executable with the given arguments + + :param args: The command arguments to give to ``bpt``. + :param cwd: The working directory of the subprocess. + :param timeout: A timeout for the subprocess's execution. + """ + env = os.environ.copy() + proc.check_run([self.path, self.always_args, args], cwd=cwd or self.default_cwd, env=env, timeout=timeout) + + def pkg_prefetch(self, *, repos: Iterable[Pathish], pkgs: Iterable[str] = ()) -> None: + "Execute the ``bpt pkg prefetch`` subcommand" + self.run(['pkg', 'prefetch', '--no-default-repo', (f'--use-repo={r}' for r in repos), pkgs]) + + def pkg_solve(self, *, repos: Iterable[Pathish], pkgs: Iterable[str]) -> None: + "Execute the ``bpt pkg solve`` subcommand" + self.run(['pkg', 'solve', '--no-default-repo', (f'--use-repo={r}' for r in repos), pkgs]) + + def build(self, + *, + root: Pathish, + toolchain: Pathish | None = None, + build_root: Pathish | None = None, + jobs: int | None = None, + tweaks_dir: Pathish | None = None, + with_tests: bool = True, + repos: Iterable[Pathish] = (), + more_args: proc.CommandLine | None = None, + timeout: float | None = None, + cwd: Pathish | None = None) -> None: + """ + Run 'bpt build' with the given arguments. + + :param root: The root project directory. + :param toolchain: The toolchain to use for the build. + :param build_root: The root directory where the output will be written. + :param jobs: The number of jobs to use. Default is CPU-count + 2 + :param tweaks_dir: Set the tweaks-dir for the build + :param with_tests: Toggle building and executing of tests. + :param repos: Repositories to use during the build. + :param more_args: Additional command-line arguments. + :param timeout: Timeout for the build subprocess. + :param cwd: Working directory for the bpt subprocess + """ + toolchain = toolchain or tc_mod.get_default_audit_toolchain() + jobs = jobs or multiprocessing.cpu_count() + 2 + self.run( + [ + 'build', + '--no-default-repo', + f'--toolchain={toolchain}', + (f'--use-repo={r}' for r in repos), + f'--jobs={jobs}', + f'--project={root}', + f'--out={build_root}', + () if with_tests else ('--no-tests'), + f'--tweaks-dir={tweaks_dir}' if tweaks_dir else (), + more_args or (), + ], + timeout=timeout, + cwd=cwd, + ) + + def compile_file(self, + paths: Iterable[Pathish], + *, + tweaks_dir: Pathish | None = None, + toolchain: Pathish | None = None, + root: Pathish, + build_root: Pathish | None = None, + more_args: proc.CommandLine = ()) -> None: + """ + Run 'bpt compile-file' for the given paths. + + .. seealso: :func:`build` for additional parameter information + """ + toolchain = toolchain or tc_mod.get_default_audit_toolchain() + self.run([ + 'compile-file', + paths, + f'--tweaks-dir={tweaks_dir}' if tweaks_dir else (), + f'--toolchain={toolchain}', + f'--project={root}', + f'--out={build_root}', + more_args, + ]) + + def build_deps(self, + args: proc.CommandLine, + *, + repos: Iterable[Pathish] = (), + toolchain: Pathish | None = None) -> None: + """ + run the ``bpt build-deps`` subcommand. + + .. seealso: :func:`build` for additional parameter information. + """ + toolchain = toolchain or tc_mod.get_default_audit_toolchain() + self.run([ + 'build-deps', + '-ltrace', + f'--toolchain={toolchain}', + (f'--use-repo={r}' for r in repos), + args, + ]) diff --git a/tools/bpt_ci/docs.py b/tools/bpt_ci/docs.py new file mode 100644 index 00000000..9532d610 --- /dev/null +++ b/tools/bpt_ci/docs.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +from pathlib import Path +import itertools +from typing import Iterable +import re +import sys +import difflib + +from . import paths + +DOCS_REF_RE = re.compile(r'(? Iterable[str]: + """Find referenced documentation pages in the given C++ code""" + code = code.replace('\n', ' ') + for mat in DOCS_ERR_RE.finditer(code): + inner = mat.group(1) + ref = eval(inner) # pylint: disable=eval-used + assert isinstance(ref, str), code + yield f'err/{ref}' + + +def doc_refs_in_file(filepath: Path) -> set[str]: + """Find referenced documenation pages in the given C++ source file""" + content = filepath.read_text(encoding='utf-8') + return set(doc_refs_in_code(content)) + + +def scan_doc_references(src_root: Path) -> dict[Path, set[str]]: + """Find referenced documentation pages in the given C++ source tree""" + patterns = ('*.h', '*.hpp', '*.c', '*.cpp') + source_files = list(itertools.chain.from_iterable(src_root.rglob(pat) for pat in patterns)) + refs_by_file = ((fpath, doc_refs_in_file(fpath)) for fpath in source_files) + return dict(refs_by_file) + + +def normalize_docname(fpath: Path, root: Path) -> str: + """ + Normalize the name of a documentation page relative to a documentation root + + :param fpath: The absolute path of the documentation page file. + :param root: The absolute path of the documentation root directory. + """ + return fpath.relative_to(root).with_suffix('').as_posix() + + +def nearest(given: str, cands: Iterable[str]) -> str | None: + """ + Find the nearest candidate string to the given string + + :param given: An input string. + :param cands: A set of candidate strings to match. + + :returns: The string in ``cands`` that is most similar to ``given``, or + ``None`` otherwise. + """ + return min(cands, key=lambda c: -difflib.SequenceMatcher(None, given, c).ratio(), default=None) + + +def audit_docrefs_main(): + """ + Entrypoint of ``bpt-audit-docrefs``. Finds documentation pages referred to + in the source code, searching for spelling errors, and searches for any + error pages that are not referenced anywhere in the source files. + """ + docs_by_file = scan_doc_references(paths.PROJECT_ROOT / 'src') + docs_root = paths.PROJECT_ROOT / 'docs' + all_docs = {normalize_docname(d, docs_root) for d in docs_root.rglob('*.rst')} + # Keep track of all referenced error docs + err_docs = {d for d in all_docs if d.startswith('err/')} + # (No one refers to the index) + err_docs.remove('err/index') + + errors = False + for fpath, docs in docs_by_file.items(): + for doc in docs: + if doc not in all_docs: + print(f'Source file [{fpath}] refers to non-existent documentation page "{doc}"') + cand = nearest(doc, all_docs) + if cand: + print(f' (Did you mean "{cand}"?)') + err_docs.discard(doc) + + for doc in err_docs: + print(f'Unreferenced error-documentation page: {doc}') + errors = True + + if errors: + sys.exit(1) diff --git a/tools/dds_ci/msvs.py b/tools/bpt_ci/msvs.py similarity index 82% rename from tools/dds_ci/msvs.py rename to tools/bpt_ci/msvs.py index c281e369..19f0832e 100644 --- a/tools/dds_ci/msvs.py +++ b/tools/bpt_ci/msvs.py @@ -2,7 +2,7 @@ import json import os from pathlib import Path -from typing import Optional, Dict, Any +from typing import Optional, Dict, Any, cast from typing_extensions import Protocol from . import paths @@ -13,8 +13,8 @@ class Arguments(Protocol): def gen_task_json_data() -> Dict[str, Any]: - dds_ci_exe = paths.find_exe('dds-ci') - assert dds_ci_exe, 'Unable to find the dds-ci executable. This command should be run in a Poetry' + dagon_exe = paths.find_exe('dagon') + assert dagon_exe, 'Unable to find the dagon executable. This command should be run in a Poetry' envs = {key: os.environ[key] for key in ( 'CL', @@ -27,8 +27,8 @@ def gen_task_json_data() -> Dict[str, Any]: task = { 'label': 'MSVC Build', 'type': 'process', - 'command': str(dds_ci_exe.resolve()), - 'args': ['--rapid'], + 'command': str(dagon_exe.resolve()), + 'args': ['build.test'], 'group': { 'kind': 'build', }, @@ -43,7 +43,7 @@ def gen_task_json_data() -> Dict[str, Any]: def generate_vsc_task() -> None: parser = argparse.ArgumentParser() parser.add_argument('--out', '-o', help='File to write into', type=Path) - args: Arguments = parser.parse_args() + args = cast(Arguments, parser.parse_args()) cl = paths.find_exe('cl') if cl is None: diff --git a/tools/dds_ci/paths.py b/tools/bpt_ci/paths.py similarity index 66% rename from tools/dds_ci/paths.py rename to tools/bpt_ci/paths.py index b4d798b6..671004da 100644 --- a/tools/dds_ci/paths.py +++ b/tools/bpt_ci/paths.py @@ -6,22 +6,24 @@ from pathlib import Path from typing import Iterator, Optional -# The root directory of the dds project PROJECT_ROOT = Path(__file__).absolute().parent.parent.parent -#: The /tools directory +'The root directory of the bpt project' TOOLS_DIR = PROJECT_ROOT / 'tools' -#: The /tests directory +'The ``/tools`` directory' TESTS_DIR = PROJECT_ROOT / 'tests' -#: The default build directory +'The ``/tests`` directory' BUILD_DIR = PROJECT_ROOT / '_build' -#: The directory were w prebuild/bootstrapped results will go, and scratch space for the build +'The default build directory' +TWEAKS_DIR = PROJECT_ROOT / 'conf' +'The directory for tweak headers used by the build' PREBUILT_DIR = PROJECT_ROOT / '_prebuilt' -#: THe suffix of executable files on this system +'The directory were prebuild/bootstrapped results will go, and scratch space for the build' EXE_SUFFIX = '.exe' if os.name == 'nt' else '' -#: The path to the prebuilt 'dds' executable -PREBUILT_DDS = (PREBUILT_DIR / 'dds').with_suffix(EXE_SUFFIX) -#: The path to the main built 'dds' executable -CUR_BUILT_DDS = (BUILD_DIR / 'dds').with_suffix(EXE_SUFFIX) +'The suffix of executable files on this system' +PREBUILT_BPT = (PREBUILT_DIR / 'bpt').with_suffix(EXE_SUFFIX) +'The path to the prebuilt ``bpt`` executable' +CUR_BUILT_BPT = (BUILD_DIR / 'bpt').with_suffix(EXE_SUFFIX) +'The path to the main built ``bpt`` executable' @contextmanager diff --git a/tools/bpt_ci/proc.py b/tools/bpt_ci/proc.py new file mode 100644 index 00000000..9ba29e08 --- /dev/null +++ b/tools/bpt_ci/proc.py @@ -0,0 +1,43 @@ +from typing import Iterable, Optional, NoReturn, Sequence, Mapping +from typing_extensions import Protocol +import subprocess + +import dagon.proc +from dagon.proc import CommandLine + +from .util import Pathish + + +class ProcessResult(Protocol): + args: Sequence[str] + returncode: int + stdout: bytes + stderr: bytes + + +def flatten_cmd(cmd: CommandLine) -> Iterable[str]: + return (str(s) for s in dagon.proc.flatten_cmdline(cmd)) + + +def run(*cmd: CommandLine, + cwd: Optional[Pathish] = None, + check: bool = False, + env: Optional[Mapping[str, str]] = None, + timeout: Optional[float] = None) -> ProcessResult: + timeout = timeout or 60 * 5 + command = list(flatten_cmd(cmd)) + res = subprocess.run(command, cwd=cwd, check=False, env=env, timeout=timeout) + if res.returncode and check: + raise_error(res) + return res + + +def raise_error(proc: ProcessResult) -> NoReturn: + raise subprocess.CalledProcessError(proc.returncode, proc.args, output=proc.stdout, stderr=proc.stderr) + + +def check_run(*cmd: CommandLine, + cwd: Optional[Pathish] = None, + env: Optional[Mapping[str, str]] = None, + timeout: Optional[float] = None) -> ProcessResult: + return run(cmd, cwd=cwd, check=True, env=env, timeout=timeout) diff --git a/tools/bpt_ci/tasks.py b/tools/bpt_ci/tasks.py new file mode 100644 index 00000000..c3363ec5 --- /dev/null +++ b/tools/bpt_ci/tasks.py @@ -0,0 +1,401 @@ +from __future__ import annotations + +import asyncio +import json +import os +import re +import sys +from pathlib import Path + +from dagon import fs, option, proc, task, ui + +from . import paths +from .bootstrap import BootstrapMode, get_bootstrap_exe, pin_exe +from .bpt import BPTWrapper +from .toolchain import (fixup_toolchain, get_default_audit_toolchain, get_default_toolchain) +from .util import Pathish + +FRAC_RE = re.compile(r'(\d+)/(\d+)') + +bootstrap_mode = option.add('bootstrap-mode', + BootstrapMode, + default=BootstrapMode.Lazy, + doc='How should we obtain the prior version of bpt?') + +main_tc = option.add('main-toolchain', + Path, + default=get_default_toolchain(), + doc='The toolchain to use when building the release executable') +test_tc = option.add('test-toolchain', + Path, + default=get_default_audit_toolchain(), + doc='The toolchain to use when building for testing') + +jobs = option.add('jobs', int, default=6, doc='Number of parallel jobs to run during build and test') + +main_build_dir = option.add('build.main.dir', + Path, + default=paths.BUILD_DIR, + doc='Directory in which to store the main build results') +test_build_dir = option.add('build.test.dir', + Path, + default=paths.BUILD_DIR / 'for-test', + doc='Directory in which to store the test build results') + +PRIOR_CACHE_ARGS = [ + f'--pkg-db-path={paths.PREBUILT_DIR/"ci-catalog.db"}', + f'--pkg-cache-dir={paths.PREBUILT_DIR / "ci-repo"}', +] + +NEW_CACHE_ARGS = [ + f'--crs-cache-dir={paths.PREBUILT_DIR / "_crs"}', +] + + +def _progress(record: proc.ProcessOutputItem) -> None: + line = record.out.decode() + ui.status(line) + mat = FRAC_RE.search(line) + if not mat: + return + num, denom = mat.groups() + ui.progress(int(num) / int(denom)) + + +async def _build_with_tc(bpt: BPTWrapper, into: Pathish, tc: Path, *, args: proc.CommandLine = ()) -> BPTWrapper: + into = Path(into) + with pin_exe(bpt.path) as pinned: + with fixup_toolchain(tc) as tc1: + ui.print(f'Generating a build of bpt using the [{tc1}] toolchain.') + ui.print(f' This build result will be written to [{into}]') + await proc.run( + [ + pinned, + 'build', + '-o', + into, + '-j', + jobs.get(), + f'--toolchain={tc1}', + '--tweaks-dir', + paths.TWEAKS_DIR, + args, + ], + on_output=_progress, + print_output_on_finish='always', + ) + return BPTWrapper(into / 'bpt') + + +@task.define() +async def clean(): + ui.status('Removing prior build') + await fs.remove( + [ + paths.BUILD_DIR, + paths.PREBUILT_DIR, + *paths.PROJECT_ROOT.rglob('__pycache__'), + *paths.PROJECT_ROOT.rglob('*.stamp'), + ], + absent_ok=True, + recurse=True, + ) + + +@task.define(order_only_depends=[clean]) +async def bootstrap() -> BPTWrapper: + ui.status('Obtaining prior BPT version') + d = get_bootstrap_exe(bootstrap_mode.get()) + ui.status('Loading legacy repository information') + await proc.run( + [ + d.path, + 'pkg', + 'repo', + 'add', + 'https://repo-1.dds.pizza', + PRIOR_CACHE_ARGS, + ], + on_output='status', + ) + return d + + +@task.define(depends=[bootstrap]) +async def build__init_ci_repo() -> None: + "Runs a compile-file just to get the CI repository available" + bpt = await task.result_of(bootstrap) + await proc.run( + [ + bpt.path, + PRIOR_CACHE_ARGS, + 'compile-file', + '-t', + main_tc.get(), + ], + on_output='status', + ) + + +@task.define(depends=[bootstrap, build__init_ci_repo]) +async def build__main() -> BPTWrapper: + bpt = await task.result_of(bootstrap) + return await _build_with_tc(bpt, main_build_dir.get(), main_tc.get(), args=[PRIOR_CACHE_ARGS, '--no-tests']) + + +@task.define(depends=[bootstrap, build__init_ci_repo]) +async def build__test() -> BPTWrapper: + bpt = await task.result_of(bootstrap) + return await _build_with_tc(bpt, test_build_dir.get(), test_tc.get(), args=PRIOR_CACHE_ARGS) + + +which_file = option.add('compile-file', Path, doc='Which file will be compiled by the "compile-file" task') + + +@task.define(depends=[bootstrap, build__init_ci_repo]) +async def compile_file() -> None: + bpt = await task.result_of(bootstrap) + with fixup_toolchain(test_tc.get()) as tc: + await proc.run([ + bpt.path, + 'compile-file', + which_file.get(), + f'--toolchain={tc}', + f'--tweaks-dir={paths.TWEAKS_DIR}', + f'--out={test_build_dir.get()}', + f'--project={paths.PROJECT_ROOT}', + PRIOR_CACHE_ARGS, + ]) + + +@task.define(depends=[build__test]) +async def test() -> None: + basetemp = Path('/tmp/bpt-ci') + basetemp.mkdir(exist_ok=True, parents=True) + bpt = await task.result_of(build__test) + await proc.run( + [ + sys.executable, + '-m', + 'pytest', + '-v', + '--durations=10', + f'-n{jobs.get()}', + f'--basetemp={basetemp}', + f'--bpt-exe={bpt.path}', + f'--junit-xml={paths.BUILD_DIR}/pytest-junit.xml', + paths.PROJECT_ROOT / 'tests', + ], + on_output='status', + print_output_on_finish='always', + ) + + +@task.define(order_only_depends=[clean]) +async def __get_catch2() -> Path: + tag = 'v2.13.7' + into = paths.BUILD_DIR / f'_catch2-{tag}' + ui.status('Cloning Catch2') + if not into.is_dir(): + await proc.run([ + 'git', 'clone', 'https://github.com/catchorg/Catch2.git', f'--branch={tag}', '--depth=1', + into.with_suffix('.tmp') + ]) + into.with_suffix('.tmp').rename(into) + proj_dir = into / '_proj' + proj_dir.mkdir(exist_ok=True) + proj_dir.joinpath('bpt.yaml').write_text( + json.dumps({ + 'name': + 'catch2', + 'version': + tag[1:], + 'libs': [{ + 'path': '.', + 'name': 'catch2', + }, { + 'path': 'mainlib', + 'name': 'main', + 'using': ['catch2'], + }], + })) + inc_dir = proj_dir / 'include' + if not inc_dir.is_dir(): + await fs.copy_tree(into / 'single_include/', inc_dir, if_exists='merge', if_file_exists='keep') + main_src = proj_dir / 'mainlib/src' + main_src.mkdir(exist_ok=True, parents=True) + main_src.joinpath('catch2_main.cpp').write_text(''' + #define CATCH_CONFIG_MAIN + #include + ''') + return proj_dir + + +@task.define(depends=[build__test, __get_catch2]) +async def self_build_repo() -> Path: + bpt = await task.result_of(build__test) + repo_dir = paths.PREBUILT_DIR / '_crs-repo' + await proc.run([bpt.path, NEW_CACHE_ARGS, 'repo', 'init', repo_dir, '--name=.tmp.', '--if-exists=ignore']) + packages = [ + "spdlog@1.7.0", + "ms-wil@2020.3.16", + "range-v3@0.11.0", + "nlohmann-json@3.9.1", + "neo-sqlite3@0.7.1", + "neo-fun^0.11.1", + "neo-buffer^0.5.2", + "neo-compress^0.3.1", + "neo-io@0.2.3", + "neo-url^0.2.5", + "semver@0.2.2", + "pubgrub^0.3.1", + "vob-json5@0.1.6", + "vob-semester@0.3.1", + "ctre@2.8.1", + "fmt^6.2.1", + "neo-http^0.2.0", + "boost.leaf^1.78.0", + "magic_enum+0.7.3", + "sqlite3^3.35.2", + "yaml-cpp@0.7.0", + "zlib@1.2.9", + ] + for pkg in packages: + await _repo_import_pkg(bpt, pkg, repo_dir) + catch2 = await task.result_of(__get_catch2) + await _repo_build_and_import_dir(bpt, catch2, repo_dir) + ui.status('Validating repository') + await proc.run([bpt.path, 'repo', 'validate', repo_dir]) + return repo_dir + + +SELF_REPO_DEPS = { + 'neo-buffer': ['neo-fun@0.11.1 using neo-fun'], + 'neo-sqlite3': ['neo-fun@0.11.1 using neo-fun', 'sqlite3@3.35.2 using sqlite3'], + 'neo-url': ['neo-fun@0.11.1 using neo-fun'], + 'neo-compress': ['neo-buffer@0.5.2 using neo-buffer', 'zlib@1.2.9 using zlib'], + 'neo-io': ['neo-buffer@0.5.2 using neo-buffer'], + 'neo-http': ['neo-io@0.2.3 using neo-io'], +} + + +async def _repo_import_pkg(bpt: BPTWrapper, pkg: str, repo_dir: Path) -> None: + mat = re.match(r'(.+)[@=+~^](.+)', pkg) + assert mat, pkg + name, ver = mat.groups() + pid = f'{name}@{ver}' + pulled = paths.PREBUILT_DIR / 'ci-repo' / pid + assert pulled.is_dir(), pid + pulled.joinpath('bpt.yaml').write_text( + json.dumps({ + 'name': name, + 'version': ver, + 'dependencies': SELF_REPO_DEPS.get(name, []), + })) + ui.status(f'Importing package: {pkg}') + await _repo_build_and_import_dir(bpt, pulled, repo_dir) + + +async def _repo_build_and_import_dir(bpt: BPTWrapper, proj: Path, repo_dir: Path) -> None: + # await proc.run([bpt.path, NEW_CACHE_ARGS, 'build', '-p', repo_dir, '-t', test_tc.get(), '-o', proj / '_build']) + await proc.run([bpt.path, NEW_CACHE_ARGS, 'repo', 'import', repo_dir, proj, '--if-exists=replace']) + + +@task.define(depends=[build__test, self_build_repo]) +async def self_build() -> BPTWrapper: + bpt = await task.result_of(build__test) + self_repo = await task.result_of(self_build_repo) + return await _build_with_tc( + bpt, + test_build_dir.get() / 'self', + main_tc.get(), + args=[f'--use-repo={self_repo.as_uri()}', NEW_CACHE_ARGS, '--no-default-repo'], + ) + + +@task.define(order_only_depends=[clean]) +async def docs() -> None: + ui.status('Building documentation with Sphinx') + await proc.run( + [ + sys.executable, + '-m', + 'sphinx', + paths.PROJECT_ROOT / 'docs', + paths.BUILD_DIR / 'docs', + '-d', + paths.BUILD_DIR / 'doctrees', + '-anj8', + ], + print_output_on_finish='always', + ) + + +def _find_clang_format() -> Path: + for cf_cand in ('clang-format-10', 'clang-format-9', 'clang-format-8', 'clang-format'): + cf = paths.find_exe(cf_cand) + if cf: + return cf + raise RuntimeError('No clang-format executable found') + + +async def _run_clang_format(args: proc.CommandLine): + cf = _find_clang_format() + await proc.run( + [cf, args, paths.PROJECT_ROOT.glob('src/**/*.[hc]pp')], + on_output='status', + ) + + +async def _run_yapf(args: proc.CommandLine): + tools_py = paths.TOOLS_DIR.rglob('*.py') + tests_py = paths.TESTS_DIR.rglob('*.py') + await proc.run( + [ + sys.executable, + '-um', + 'yapf', + args, + tools_py, + tests_py, + ], + on_output='status', + ) + + +@task.define() +async def format__cpp(): + await _run_clang_format(['-i', '--verbose']) + + +@task.define() +async def format__py(): + await _run_yapf(['--in-place', '--verbose']) + + +@task.define(order_only_depends=[format__cpp]) +async def format__cpp__check(): + ui.status("Checking C++ formatting...") + await _run_clang_format(['--Werror', '--dry-run']) + + +@task.define(order_only_depends=[format__py]) +async def format__py__check(): + ui.status('Checking Python formatting...') + await _run_yapf(['--diff']) + + +format_ = task.gather('format', [format__cpp, format__py]) +format__check = task.gather('format.check', [format__cpp__check, format__py__check]) + +pyright = proc.cmd_task('pyright', [sys.executable, '-m', 'pyright', paths.TOOLS_DIR]) +pylint = proc.cmd_task('pylint', [sys.executable, '-m', 'pylint', paths.TOOLS_DIR]) + +py = task.gather('py', [format__py__check, pylint, pyright]) + +ci = task.gather('ci', [test, build__main, py]) + +if os.name == 'nt': + # Workaround: Older Python does not set the event loop such that Windows can + # use subprocesses asynchronously + asyncio.set_event_loop(asyncio.ProactorEventLoop()) diff --git a/tools/bpt_ci/testing/__init__.py b/tools/bpt_ci/testing/__init__.py new file mode 100644 index 00000000..743e769f --- /dev/null +++ b/tools/bpt_ci/testing/__init__.py @@ -0,0 +1,14 @@ +from .fixtures import Project, ProjectOpener, ProjectYAML +from .fs import dir_renderer, DirRenderer, fs_render_cache_dir +from .repo import CRSRepo, CRSRepoFactory + +__all__ = ( + 'Project', + 'ProjectOpener', + 'ProjectYAML', + 'dir_renderer', + 'DirRenderer', + 'fs_render_cache_dir', + 'CRSRepo', + 'CRSRepoFactory', +) diff --git a/tools/bpt_ci/testing/error.py b/tools/bpt_ci/testing/error.py new file mode 100644 index 00000000..e03c5e81 --- /dev/null +++ b/tools/bpt_ci/testing/error.py @@ -0,0 +1,79 @@ +""" +Test utilities for error checking +""" + +import shutil +from contextlib import contextmanager +from typing import ContextManager, Iterator, Callable, Pattern, Union +import subprocess +from pathlib import Path +import tempfile +import os +import re + + +@contextmanager +def expect_error_marker_pred(pred: Callable[[str], bool], expected: str) -> Iterator[None]: + """ + A context-manager function that should wrap a scope that causes an error + from ``bpt``. + + :param pred: A predicate which checks if the marker passes + :param expected: A string description of the expected marker + + The wrapped scope should raise :class:`subprocess.CalledProcessError`. + + After handling the exception, asserts that the subprocess wrote an + error marker matching ``pred``. + """ + tdir = Path(tempfile.mkdtemp()) + err_file = tdir / 'error' + env_key = 'BPT_WRITE_ERROR_MARKER' + prev = os.environ.get(env_key) + try: + os.environ[env_key] = str(err_file) + yield + assert False, 'bpt subprocess did not raise CallProcessError' + except subprocess.CalledProcessError: + assert err_file.exists(), \ + f'No error marker file [{err_file}] was generated, but bpt exited with an error (Expected "{expected}")' + marker = err_file.read_text(encoding='utf-8').strip() + assert pred(marker), \ + f'bpt did not produce the expected error (Expected {expected}, got {marker})' + finally: + shutil.rmtree(tdir) + if prev: + os.environ[env_key] = prev + else: + os.environ.pop(env_key) + + +def expect_error_marker(expect: str) -> ContextManager[None]: + """ + A context-manager function that should wrap a scope that causes an error + from ``bpt``. + + :param expect: A string description of the expected marker + + The wrapped scope should raise :class:`subprocess.CalledProcessError`. + + After handling the exception, asserts that the subprocess wrote an + error marker containing the string given in ``expect``. + """ + return expect_error_marker_pred(expect.__eq__, expect) + + +def expect_error_marker_re(expect: Union[str, Pattern[str]]) -> ContextManager[None]: + """ + A context-manager function that should wrap a scope that causes an error + from ``bpt``. + + :param expect: A regular expression that the expected marker should match + + The wrapped scope should raise :class:`subprocess.CalledProcessError`. + + After handling the exception, asserts that the subprocess wrote an + error marker matching ``expect``. + """ + expect_: Pattern[str] = re.compile(expect) + return expect_error_marker_pred(lambda s: (expect_.search(s) is not None), expect_.pattern) diff --git a/tools/bpt_ci/testing/fixtures.py b/tools/bpt_ci/testing/fixtures.py new file mode 100644 index 00000000..1dd8f0c1 --- /dev/null +++ b/tools/bpt_ci/testing/fixtures.py @@ -0,0 +1,467 @@ +""" +Test fixtures used by BPT in pytest +""" + +from __future__ import annotations +from copy import deepcopy + +import json +import shutil +from contextlib import ExitStack +from pathlib import Path +from typing import (Any, Callable, Iterable, Iterator, KeysView, Mapping, Optional, Sequence, Union, cast) + +import pytest +from _pytest.config import Config as PyTestConfig +from pytest import FixtureRequest, TempPathFactory +from typing_extensions import Literal, TypedDict + +from bpt_ci.testing.fs import DirRenderer, TreeData + +from .. import paths, toolchain +from ..bpt import BPTWrapper +from ..proc import check_run +from ..util import JSONishArray, JSONishDict, JSONishValue, Pathish + +tc_mod = toolchain + + +def ensure_absent(path: Pathish) -> None: + path = Path(path) + if path.is_dir(): + shutil.rmtree(path) + elif path.exists(): + path.unlink() + else: + # File does not exist, wo we are safe to ignore it + pass + + +_ProjectYAMLLibraryUsingItem = TypedDict('_ProjectYAMLLibraryUsingItem', { + 'lib': str, +}) + + +class _ProjectYAMLDependencyItemRequired(TypedDict): + dep: str + + +class _VersionItem(TypedDict): + low: str + high: str + + +_ProjectYAMLDependencyItemOpt = TypedDict( + '_ProjectJSONDependencyItemOpt', + { + 'versions': Sequence[_VersionItem], + 'using': Sequence[str], + }, + total=False, +) + + +class _ProjectYAMLDependencyItemMap(_ProjectYAMLDependencyItemRequired, _ProjectYAMLDependencyItemOpt): + pass + + +_ProjectYAMLDependencyItem = Union[str, _ProjectYAMLDependencyItemMap] + + +class _ProjectYAMLLibraryItemRequired(TypedDict): + name: str + path: str + + +class _ProjectYAMLLibraryItem(_ProjectYAMLLibraryItemRequired, total=False): + using: Sequence[Union[str, _ProjectYAMLLibraryUsingItem]] + dependencies: Sequence[_ProjectYAMLDependencyItem] + + +class _ProjectYAMLRequired(TypedDict): + name: str + version: str + + +class ProjectYAML(_ProjectYAMLRequired, total=False): + dependencies: Sequence[_ProjectYAMLDependencyItem] + libraries: Sequence[_ProjectYAMLLibraryItem] + + +class Library: + """ + Utilities to access a library under libs/ for a Project. + """ + + def __init__(self, name: str, dirpath: Path) -> None: + self.name = name + self.root = dirpath + + def write(self, path: Pathish, content: str) -> Path: + """ + Write the given ``content`` to ``path``. If ``path`` is relative, it will + be resolved relative to the root directory of this library. + """ + path = Path(path) + if not path.is_absolute(): + path = self.root / path + path.parent.mkdir(exist_ok=True, parents=True) + path.write_text(content, encoding='utf-8') + return path + + +class _WritebackData: + + def __init__(self, fpath: Path, items: JSONishDict | JSONishArray, root: JSONishDict) -> None: + self._root = root + self._data = items + self._fpath = fpath + + def __setitem__(self, k: str | int, v: JSONishValue) -> None: + if isinstance(self._data, Sequence): + assert isinstance(k, int) + self._data[k] = v + else: + assert isinstance(k, str) + self._data[k] = v + self._writeback() + + def __getitem__(self, k: str | int) -> JSONishValue: + if isinstance(self._data, Sequence): + assert isinstance(k, int) + v = self._data[k] + else: + assert isinstance(k, str) + v = self._data[k] + return self._wrap_if_mutable(v) + + def __contains__(self, v: Any) -> bool: + return v in self._data + + def _wrap_if_mutable(self, v: JSONishValue) -> JSONishValue: + if isinstance(v, (Sequence, Mapping)) and not isinstance(v, str): + return cast(JSONishValue, _WritebackData(self._fpath, v, self._root)) + return v + + def keys(self) -> KeysView[str]: + assert isinstance(self._data, Mapping) + return self._data.keys() + + def values(self) -> Iterable[JSONishValue]: + assert isinstance(self._data, Mapping) + return (self._wrap_if_mutable(v) for v in self._data.values()) + + def items(self) -> Iterable[tuple[str, JSONishValue]]: + assert isinstance(self._data, Mapping) + for key, val in self._data.items(): + yield key, self._wrap_if_mutable(val) + + def append(self, value: JSONishValue) -> None: + assert isinstance(self._data, Sequence) + self._data.append(deepcopy(value)) + self._writeback() + + __none = object() + + def pop(self, index: str | int | None = None, default: JSONishValue | object = __none) -> JSONishValue: + d = self._data + if isinstance(d, Mapping): + assert isinstance(index, str) + if default is not self.__none: + v = d.pop(index) + else: + v = cast(JSONishValue, d.pop(index, default)) + elif index is None: + v = d.pop() + else: + assert isinstance(index, int) + v = d.pop(index) + self._writeback() + return v + + def __delitem__(self, key: str) -> None: + self.pop(key) + + def __iter__(self) -> Iterator[JSONishValue]: + return iter(self._data) + + def __len__(self) -> int: + return len(self._data) + + def get(self, key: Any, default: Any = None) -> Any: + if key in self._data: + v = self._data[key] + if isinstance(v, (Sequence, Mapping)) and not isinstance(v, str): + return _WritebackData(self._fpath, v, self._root) + return v + return default + + def insert(self, index: int, value: JSONishValue) -> None: + assert isinstance(self._data, Sequence) + self._data.insert(index, deepcopy(value)) + self._writeback() + + def _writeback(self) -> None: + self._fpath.write_text(json.dumps(self._root, indent=2)) + + def __repr__(self) -> str: + return f'' + + +_WritebackData(Path(''), {}, {}) + + +class Project: + """ + Utilities to access a project being used as a test. + """ + + def __init__(self, dirpath: Path, bpt: BPTWrapper) -> None: + self.bpt = bpt.clone() + self.root = dirpath + self.build_root = dirpath / '_build' + + @property + def bpt_yaml(self) -> ProjectYAML: + """ + Get/set the content of the ``bpt.yaml`` file for the project. + """ + dat = json.loads(self.root.joinpath('bpt.yaml').read_text()) + return cast(ProjectYAML, _WritebackData(self.root.joinpath('bpt.yaml'), dat, dat)) + + @bpt_yaml.setter + def bpt_yaml(self, data: ProjectYAML) -> None: + self.root.joinpath('bpt.yaml').write_text(json.dumps(data, indent=2)) + + def lib(self, name: str) -> Library: + return Library(name, self.root / f'libs/{name}') + + @property + def __root_library(self) -> Library: + """ + The root/default library for this project + """ + return Library('', self.root) + + @property + def project_dir_arg(self) -> str: + """Argument for --project""" + return f'--project={self.root}' + + def build(self, + *, + toolchain: Optional[Pathish] = None, + fixup_toolchain: bool = True, + jobs: Optional[int] = None, + timeout: Union[float, None] = None, + tweaks_dir: Optional[Path] = None, + with_tests: bool = True, + repos: Sequence[Pathish] = (), + log_level: Literal['info', 'debug', 'trace'] = 'trace', + cwd: Pathish | None = None) -> None: + """ + Execute 'bpt build' on the project + """ + with ExitStack() as scope: + if fixup_toolchain: + toolchain = scope.enter_context(tc_mod.fixup_toolchain(toolchain + or tc_mod.get_default_test_toolchain())) + self.bpt.build(root=self.root, + build_root=self.build_root, + toolchain=toolchain, + jobs=jobs, + timeout=timeout, + tweaks_dir=tweaks_dir, + with_tests=with_tests, + repos=repos, + more_args=[f'--log-level={log_level}'], + cwd=cwd) + + def compile_file(self, *paths: Pathish, toolchain: Optional[Pathish] = None) -> None: + with tc_mod.fixup_toolchain(toolchain or tc_mod.get_default_test_toolchain()) as tc: + self.bpt.compile_file(paths, toolchain=tc, build_root=self.build_root, root=self.root) + + def pkg_create(self, *, dest: Optional[Pathish] = None, if_exists: Optional[str] = None) -> None: + self.build_root.mkdir(exist_ok=True, parents=True) + self.bpt.run( + [ + 'pkg', + 'create', + self.project_dir_arg, + f'--out={dest}' if dest else (), + f'--if-exists={if_exists}' if if_exists else (), + ], + cwd=self.build_root, + ) + + def write(self, path: Pathish, content: str) -> Path: + """ + Write the given ``content`` to ``path``. If ``path`` is relative, it will + be resolved relative to the root directory of this project. + """ + return self.__root_library.write(path, content) + + +@pytest.fixture(scope='module') +def test_parent_dir(request: FixtureRequest) -> Path: + """ + :class:`pathlib.Path` fixture pointing to the parent directory of the file + containing the test that is requesting the current fixture + """ + return fixture_fspath(request).parent + + +def fixture_fspath(req: FixtureRequest) -> Path: + """ + Return the ``fspath`` associated with the given test fixture request. + + .. note:: This function is only a workaround for a missing type annotation on ``FixtureRequest`` + """ + return Path(getattr(req, 'fspath')) + + +class ProjectOpener(): + """ + A test fixture that opens project directories for testing + """ + + # pylint: disable=too-many-arguments + def __init__(self, bpt: BPTWrapper, request: FixtureRequest, worker: str, tmp_path_factory: TempPathFactory, + dir_renderer: DirRenderer) -> None: + self.bpt = bpt + self._request = request + self._worker_id = worker + self._tmppath_fac = tmp_path_factory + self._dir_render = dir_renderer + + @property + def test_name(self) -> str: + """The name of the test that requested this opener""" + func: Any = self._request.function # type: ignore + return func.__name__ # type: ignore + + @property + def test_dir(self) -> Path: + """The directory that contains the test that requested this opener""" + return fixture_fspath(self._request).parent + + def open(self, dirpath: Pathish) -> Project: + """ + Open a new project testing fixture from the given project directory. + + :param dirpath: The directory that contains the project to use. + + Clones the given directory and then opens a project within that clone. + The clone directory will be destroyed when the test fixture is torn down. + """ + dirpath = Path(dirpath) + if not dirpath.is_absolute(): + dirpath = self.test_dir / dirpath + + proj_copy = self.test_dir / '__test_project' + if self._worker_id != 'master': + proj_copy: Path = self._tmppath_fac.mktemp('test-project-') / self.test_name + else: + self._request.addfinalizer(lambda: ensure_absent(proj_copy)) + + shutil.copytree(dirpath, proj_copy) + new_bpt = self.bpt.clone() + + if self._worker_id == 'master': + crs_dir = self.test_dir / '__test_crs' + else: + crs_dir: Path = self._tmppath_fac.mktemp('test-crs-') / self.test_name + + new_bpt.crs_cache_dir = crs_dir + new_bpt.default_cwd = proj_copy + self._request.addfinalizer(lambda: ensure_absent(crs_dir)) + + return Project(proj_copy, new_bpt) + + def render(self, name: str, tree: TreeData) -> Project: + dirpath = self._dir_render.get_or_render(name, tree) + return self.open(dirpath) + + +@pytest.fixture() +def project_opener(request: FixtureRequest, worker_id: str, bpt: BPTWrapper, tmp_path_factory: TempPathFactory, + dir_renderer: DirRenderer) -> ProjectOpener: + """ + A fixture factory that can open directories as Project objects for building + and testing. Duplicates the project directory into a temporary location so + that the original test directory remains unchanged. + """ + opener = ProjectOpener(bpt, request, worker_id, tmp_path_factory, dir_renderer) + return opener + + +@pytest.fixture() +def tmp_project(request: FixtureRequest, worker_id: str, project_opener: ProjectOpener, + tmp_path_factory: TempPathFactory) -> Project: + """ + A fixture that generates an empty temporary project directory that will be thrown away + when the test completes. + """ + if worker_id != 'master': + proj_dir: Path = tmp_path_factory.mktemp('temp-project') + return project_opener.open(proj_dir) + + proj_dir = project_opener.test_dir / '__test_project_empty' + ensure_absent(proj_dir) + proj_dir.mkdir() + proj = project_opener.open(proj_dir) + request.addfinalizer(lambda: ensure_absent(proj_dir)) + return proj + + +@pytest.fixture(scope='session') +def bpt(bpt_exe: Path) -> BPTWrapper: + """ + A :class:`~bpt_ci.bpt.BPTWrapper` around the bpt executable under test + """ + wr = BPTWrapper(bpt_exe) + return wr + + +@pytest.fixture(scope='session') +def bpt_exe(pytestconfig: PyTestConfig) -> Path: + """A :class:`pathlib.Path` pointing to the BPT executable under test""" + opt = pytestconfig.getoption('--bpt-exe') or paths.BUILD_DIR / 'for-test/bpt' + assert isinstance(opt, (Path, str)) + return Path(opt) + + +TmpGitRepoFactory = Callable[[Pathish], Path] + + +@pytest.fixture(scope='session') +def tmp_git_repo_factory(tmp_path_factory: TempPathFactory, pytestconfig: PyTestConfig) -> TmpGitRepoFactory: + """ + A temporary directory :class:`pathlib.Path` object in which a git repo will + be initialized + """ + + def f(dirpath: Pathish) -> Path: + test_dir = Path() + dirpath = Path(dirpath) + if not dirpath.is_absolute(): + dirpath = test_dir / dirpath + + tmp_path: Path = tmp_path_factory.mktemp('tmp-git') + + # Could use dirs_exists_ok=True with Python 3.8, but we min dep on 3.6 + repo = tmp_path / 'r' + shutil.copytree(dirpath, repo) + + git = pytestconfig.getoption('--git-exe') or 'git' + assert isinstance(git, str) + check_run([git, 'init', repo]) + check_run([git, 'checkout', '-b', 'tmp_git_repo'], cwd=repo) + check_run([git, 'add', '-A'], cwd=repo) + check_run( + [git, '-c', "user.name='Tmp Git'", '-c', "user.email='bpt@example.org'", 'commit', '-m', 'Initial commit'], + cwd=repo) + + return repo + + return f diff --git a/tools/bpt_ci/testing/fs.py b/tools/bpt_ci/testing/fs.py new file mode 100644 index 00000000..9684a23b --- /dev/null +++ b/tools/bpt_ci/testing/fs.py @@ -0,0 +1,142 @@ +from pathlib import Path +import time +from contextlib import contextmanager +from typing import Callable, ContextManager, Dict, Iterator, NamedTuple, Optional, Union +import pytest +import hashlib +import base64 +import json +import shutil + +from pytest import TempPathFactory + +from ..util import Pathish +from ..paths import PROJECT_ROOT + + +@pytest.fixture(scope='session') +def fs_render_cache_dir() -> Path: + return PROJECT_ROOT / '_build/_test/dircache' + + +TreeData = Dict[str, Union[str, bytes, 'TreeData']] +_B64TreeData = Dict[str, Union[str, '_B64TreeData']] + + +def _b64_encode_tree(tree: TreeData) -> _B64TreeData: + r: _B64TreeData = {} + for key, val in tree.items(): + if isinstance(val, str): + val = val.encode("utf-8") + if isinstance(val, bytes): + r[key] = base64.b64encode(val).decode('utf-8') + else: + r[key] = _b64_encode_tree(val) + return r + + +class GetResult(NamedTuple): + ready_path: Optional[Path] + """ + If not `None`, the path to the already-generated directory. Returned by + `DirRenderer.get_or_prepare` + """ + prep_path: Optional[Path] + + final_dest: Path + + def commit(self) -> Path: + assert self.prep_path + self.prep_path.rename(self.final_dest) + return self.final_dest + + +def render_into(root: Pathish, tree: TreeData) -> Path: + root = Path(root) + root.mkdir(exist_ok=True, parents=True) + for key, val in tree.items(): + child = root / key + if isinstance(val, str): + child.write_text(val, encoding='utf-8') + elif isinstance(val, bytes): + child.write_bytes(val) + else: + render_into(child, val) + return root + + +class DirRenderer: + """ + Utility for generating filesystem structures and restoring them from a cache. + + :param cache_root: The root directory where rendered content will be stored. + :param tmp_root: Root directory for temporary files. + """ + + def __init__(self, cache_root: Pathish, tmp_root: Pathish) -> None: + self._cache_path = Path(cache_root) + self._tmp_path = Path(tmp_root) + + def get_or_prepare(self, key: str) -> ContextManager[GetResult]: + """ + Open a context manager to a directory that may or may not already be cached. + + If `GetResult.ready_path` is not :data:`None`, the directory is already + present in the cache. + """ + return self._get_or_prepare(key) + + @contextmanager + def _get_or_prepare(self, key: str) -> Iterator[GetResult]: + cached_path = self._cache_path / key + if cached_path.is_dir(): + yield GetResult(cached_path, None, cached_path) + return + + prep_path = cached_path.with_suffix(cached_path.suffix + '.tmp') + try: + prep_path.mkdir(parents=True) + except FileExistsError: + later = time.time() + 10 + while prep_path.exists() and time.time() < later: + pass + assert cached_path.is_dir() + yield GetResult(cached_path, None, cached_path) + return + + yield GetResult(None, prep_path, cached_path) + shutil.rmtree(prep_path, ignore_errors=True) + + def get_or_render(self, name: str, tree: TreeData) -> Path: + b64_tree = _b64_encode_tree(tree) + md5 = hashlib.md5(json.dumps(b64_tree, sort_keys=True).encode('utf-8')).hexdigest()[:4] + key = f'{name}-{md5}' + clone_dest = self._tmp_path / key + shutil.rmtree(clone_dest, ignore_errors=True) + with self.get_or_prepare(key) as prep: + if prep.ready_path: + shutil.copytree(prep.ready_path, clone_dest) + return clone_dest + assert prep.prep_path + render_into(prep.prep_path, tree) + return prep.commit() + + +@pytest.fixture(scope='session') +def dir_renderer(fs_render_cache_dir: Path, tmp_path_factory: TempPathFactory) -> DirRenderer: + tp: Path = tmp_path_factory.mktemp('render-clones') + return DirRenderer(fs_render_cache_dir, tp) + + +TempCloner = Callable[[str, Pathish], Path] + + +@pytest.fixture(scope='session') +def tmp_clone_dir(tmp_path_factory: TempPathFactory) -> TempCloner: + + def _dup(name: str, p: Pathish) -> Path: + tdir: Path = tmp_path_factory.mktemp(name) / '_' + shutil.copytree(p, tdir) + return tdir + + return _dup diff --git a/tools/bpt_ci/testing/http.py b/tools/bpt_ci/testing/http.py new file mode 100644 index 00000000..9104b24d --- /dev/null +++ b/tools/bpt_ci/testing/http.py @@ -0,0 +1,77 @@ +import socket +from concurrent.futures import ThreadPoolExecutor +from contextlib import ExitStack, closing, contextmanager +from functools import partial +from http.server import HTTPServer, SimpleHTTPRequestHandler +from pathlib import Path +from typing import Any, Callable, Iterator, NamedTuple, cast + +import pytest +from pytest import FixtureRequest + + +def _unused_tcp_port() -> int: + """Find an unused localhost TCP port from 1024-65535 and return it.""" + with closing(socket.socket()) as sock: + sock.bind(('127.0.0.1', 0)) + return cast(int, sock.getsockname()[1]) + + +class DirectoryServingHTTPRequestHandler(SimpleHTTPRequestHandler): + """ + A simple HTTP request handler that simply serves files from a directory given to the constructor. + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + self.dir = kwargs.pop('dir') + super().__init__(*args, **kwargs) + + def translate_path(self, path: str) -> str: + # Convert the given URL path to a path relative to the directory we are serving + abspath = Path(super().translate_path(path)) # type: ignore + relpath = abspath.relative_to(Path.cwd()) + return str(self.dir / relpath) + + +class ServerInfo(NamedTuple): + """ + Information about an HTTP server fixture + """ + base_url: str + root: Path + + +@contextmanager +def run_http_server(dirpath: Path, port: int) -> Iterator[ServerInfo]: + """ + Context manager that spawns an HTTP server that serves thegiven directory on + the given TCP port. + """ + handler = partial(DirectoryServingHTTPRequestHandler, dir=dirpath) + addr = ('127.0.0.1', port) + pool = ThreadPoolExecutor() + with HTTPServer(addr, handler) as httpd: + pool.submit(lambda: httpd.serve_forever(poll_interval=0.1)) + try: + print('Serving at', addr) + yield ServerInfo(f'http://127.0.0.1:{port}', dirpath) + finally: + httpd.shutdown() + + +HTTPServerFactory = Callable[[Path], ServerInfo] + + +@pytest.fixture(scope='session') +def http_server_factory(request: FixtureRequest) -> HTTPServerFactory: + """ + Spawn an HTTP server that serves the content of a directory. + """ + + def _make(p: Path) -> ServerInfo: + st = ExitStack() + server = st.enter_context(run_http_server(p, _unused_tcp_port())) + request.addfinalizer(st.pop_all) + return server + + return _make diff --git a/tools/bpt_ci/testing/repo.py b/tools/bpt_ci/testing/repo.py new file mode 100644 index 00000000..0d4f7884 --- /dev/null +++ b/tools/bpt_ci/testing/repo.py @@ -0,0 +1,128 @@ +""" +Test fixtures and utilities for creating and using CRS repositories +""" + +from pathlib import Path +from typing import Any, Callable, Iterable, NamedTuple, Union + +import pytest +from typing_extensions import Literal + +from ..bpt import BPTWrapper +from .fixtures import TempPathFactory +from .fs import TempCloner +from .http import HTTPServerFactory, ServerInfo + + +class CRSRepo: + """ + A CRS repository directory + """ + + def __init__(self, path: Path, bpt: BPTWrapper) -> None: + self.path = path + self.bpt = bpt + + def import_(self, + path: Union[Path, Iterable[Path]], + *, + if_exists: Literal[None, 'replace', 'ignore', 'fail'] = None, + validate: bool = True) -> None: + if isinstance(path, Path): + path = [path] + self.bpt.run([ + '-ldebug', + 'repo', + 'import', + self.path, + path, + (() if if_exists is None else f'--if-exists={if_exists}'), + ]) + if validate: + self.validate() + + def remove(self, name: str, if_missing: Literal[None, 'ignore', 'fail'] = None) -> None: + self.bpt.run([ + '-ldebug', + 'repo', + 'remove', + self.path, + name, + (() if if_missing is None else f'--if-missing={if_missing}'), + ]) + + def validate(self) -> None: + self.bpt.run(['repo', 'validate', self.path, '-ldebug']) + + +CRSRepoFactory = Callable[[str], CRSRepo] + + +@pytest.fixture(scope='session') +def crs_repo_factory(tmp_path_factory: TempPathFactory, bpt: BPTWrapper) -> CRSRepoFactory: + + def _make(name: str) -> CRSRepo: + tmpdir = Path(tmp_path_factory.mktemp('crs-repo-')) + bpt.run(['repo', 'init', tmpdir, f'--name={name}']) + return CRSRepo(tmpdir, bpt) + + return _make + + +@pytest.fixture(scope='session') +def session_empty_crs_repo(crs_repo_factory: CRSRepoFactory) -> CRSRepo: + return crs_repo_factory('session-empty') + + +RepoCloner = Callable[[CRSRepo], CRSRepo] + + +@pytest.fixture(scope='session') +def clone_repo(tmp_clone_dir: TempCloner) -> RepoCloner: + + def _clone(repo: CRSRepo) -> CRSRepo: + clone = tmp_clone_dir('repo', repo.path) + return CRSRepo(clone, repo.bpt) + + return _clone + + +@pytest.fixture() +def tmp_crs_repo(session_empty_crs_repo: CRSRepo, clone_repo: RepoCloner) -> CRSRepo: + return clone_repo(session_empty_crs_repo) + + +class CRSRepoServer(NamedTuple): + """ + An HTTP-server CRS repository + """ + repo: CRSRepo + server: ServerInfo + + +@pytest.fixture() +def http_crs_repo(tmp_crs_repo: CRSRepo, http_server_factory: HTTPServerFactory) -> CRSRepoServer: + """Generate a temporary HTTP server serving a temporary CRS repository""" + server = http_server_factory(tmp_crs_repo.path) + return CRSRepoServer(tmp_crs_repo, server) + + +def make_simple_crs(name: str, version: str, *, pkg_version: int = 1) -> Any: + return { + 'schema-version': + 0, + 'name': + name, + 'version': + version, + 'pkg-version': + pkg_version, + 'libraries': [{ + 'path': '.', + 'name': name, + 'using': [], + 'test-using': [], + 'dependencies': [], + 'test-dependencies': [], + }] + } diff --git a/tools/dds_ci/toolchain.py b/tools/bpt_ci/toolchain.py similarity index 58% rename from tools/dds_ci/toolchain.py rename to tools/bpt_ci/toolchain.py index 8121fb87..23b4b12f 100644 --- a/tools/dds_ci/toolchain.py +++ b/tools/bpt_ci/toolchain.py @@ -2,13 +2,12 @@ import sys from contextlib import contextmanager from pathlib import Path -from typing import Iterator +from typing import Iterator, Mapping, MutableSequence, cast -import distro import json5 from . import paths -from .util import Pathish +from .util import JSONishDict, Pathish @contextmanager @@ -19,20 +18,23 @@ def fixup_toolchain(json_file: Pathish) -> Iterator[Path]: based on 'json_file' """ json_file = Path(json_file) - data = json5.loads(json_file.read_text()) + data: JSONishDict = json5.loads(json_file.read_text('utf-8')) # type: ignore + assert isinstance(data, Mapping) # Check if we can add ccache ccache = paths.find_exe('ccache') if ccache and data.get('compiler_id') in ('gnu', 'clang'): - print('Found ccache:', ccache) - data['compiler_launcher'] = [str(ccache)] + data['compiler_launcher'] = [str(ccache)] # type: ignore # Check for lld for use with GCC/Clang - if paths.find_exe('ld.lld') and data.get('compiler_id') in ('gnu', 'clang'): - print('Linking with `-fuse-ld=lld`') - data.setdefault('link_flags', []).append('-fuse-ld=lld') + link_flags = cast('list[str]', data.setdefault('link_flags', [])) + assert isinstance(link_flags, list) and all(isinstance(flag, str) for flag in link_flags) + uses_lto = any(flag.startswith('-flto') for flag in link_flags) + if not uses_lto and paths.find_exe('ld.lld') and data.get('compiler_id') in ('gnu', 'clang'): + assert isinstance(link_flags, MutableSequence), link_flags + link_flags.append('-fuse-ld=lld') # Save the new toolchain data with paths.new_tempdir() as tdir: new_json = tdir / json_file.name - new_json.write_text(json.dumps(data)) + new_json.write_text(json.dumps(data), encoding='utf-8') yield new_json @@ -44,21 +46,25 @@ def get_default_audit_toolchain() -> Path: if sys.platform == 'win32': return paths.TOOLS_DIR / 'msvc-audit.jsonc' if sys.platform == 'linux': - return paths.TOOLS_DIR / 'gcc-9-audit.jsonc' + return paths.TOOLS_DIR / 'gcc-10-audit.jsonc' if sys.platform == 'darwin': - return paths.TOOLS_DIR / 'gcc-9-audit-macos.jsonc' + return paths.TOOLS_DIR / 'gcc-10-audit-macos.jsonc' + if sys.platform == 'freebsd11': + return paths.TOOLS_DIR / 'freebsd-gcc-10.jsonc' raise RuntimeError(f'Unable to determine the default toolchain (sys.platform is {sys.platform!r})') def get_default_test_toolchain() -> Path: """ Get the default toolchain that should be used by tests that need a toolchain - to use for executing dds. + to use for executing bpt. """ if sys.platform == 'win32': return paths.TESTS_DIR / 'msvc.tc.jsonc' if sys.platform in ('linux', 'darwin'): - return paths.TESTS_DIR / 'gcc-9.tc.jsonc' + return paths.TESTS_DIR / 'gcc-10.tc.jsonc' + if sys.platform == 'freebsd11': + return paths.TOOLS_DIR / 'freebsd-gcc-10.jsonc' raise RuntimeError(f'Unable to determine the default toolchain (sys.platform is {sys.platform!r})') @@ -70,7 +76,9 @@ def get_default_toolchain() -> Path: if sys.platform == 'win32': return paths.TOOLS_DIR / 'msvc-rel.jsonc' if sys.platform == 'linux': - return paths.TOOLS_DIR / 'gcc-9-rel.jsonc' + return paths.TOOLS_DIR / 'gcc-10-rel.jsonc' if sys.platform == 'darwin': - return paths.TOOLS_DIR / 'gcc-9-rel-macos.jsonc' + return paths.TOOLS_DIR / 'gcc-10-rel-macos.jsonc' + if sys.platform == 'freebsd11': + return paths.TOOLS_DIR / 'freebsd-gcc-10.jsonc' raise RuntimeError(f'Unable to determine the default toolchain (sys.platform is {sys.platform!r})') diff --git a/tools/bpt_ci/util.py b/tools/bpt_ci/util.py new file mode 100644 index 00000000..bb63f7e1 --- /dev/null +++ b/tools/bpt_ci/util.py @@ -0,0 +1,18 @@ +from pathlib import PurePath +from typing import Union, MutableSequence, MutableMapping + +import tarfile + +#: A path, string, or convertible-to-Path object +Pathish = Union[PurePath, str] + +JSONishValue = Union[None, float, str, bool, MutableSequence['JSONishValue'], MutableMapping[str, 'JSONishValue']] +JSONishDict = MutableMapping[str, JSONishValue] +JSONishArray = MutableSequence[JSONishValue] + + +def read_tarfile_member(tarpath: Pathish, member: Pathish) -> bytes: + with tarfile.open(tarpath, 'r:*') as tf: + io = tf.extractfile(str(PurePath(member).as_posix())) + assert io, f'No member of [{tarpath}]: [{member}]' + return io.read() diff --git a/tools/dds_ci/bootstrap.py b/tools/dds_ci/bootstrap.py deleted file mode 100644 index 18ebe721..00000000 --- a/tools/dds_ci/bootstrap.py +++ /dev/null @@ -1,85 +0,0 @@ -import enum -from pathlib import Path -from contextlib import contextmanager -from typing import Iterator -import sys -import urllib.request -import shutil - -from . import paths -from .dds import DDSWrapper -from .paths import new_tempdir - - -class BootstrapMode(enum.Enum): - """How should be bootstrap our prior DDS executable?""" - #: Downlaod one from GitHub - Download = 'download' - #: Build one from source - Build = 'build' - #: Skip bootstrapping. Assume it already exists. - Skip = 'skip' - #: If the prior executable exists, skip, otherwise download - Lazy = 'lazy' - - -def _do_bootstrap_download() -> Path: - filename = { - 'win32': 'dds-win-x64.exe', - 'linux': 'dds-linux-x64', - 'darwin': 'dds-macos-x64', - 'freebsd11': 'dds-freebsd-x64', - 'freebsd12': 'dds-freebsd-x64', - }.get(sys.platform) - if filename is None: - raise RuntimeError(f'We do not have a prebuilt DDS binary for the "{sys.platform}" platform') - url = f'https://github.com/vector-of-bool/dds/releases/download/0.1.0-alpha.4/{filename}' - - print(f'Downloading prebuilt DDS executable: {url}') - stream = urllib.request.urlopen(url) - paths.PREBUILT_DDS.parent.mkdir(exist_ok=True, parents=True) - with paths.PREBUILT_DDS.open('wb') as fd: - while True: - buf = stream.read(1024 * 4) - if not buf: - break - fd.write(buf) - - if sys.platform != 'win32': - # Mark the binary executable. By default it won't be - mode = paths.PREBUILT_DDS.stat().st_mode - mode |= 0b001_001_001 - paths.PREBUILT_DDS.chmod(mode) - - return paths.PREBUILT_DDS - - -@contextmanager -def pin_exe(fpath: Path) -> Iterator[Path]: - """ - Create a copy of 'fpath' at an unspecified location, and yield that path. - - This is needed if the executable would overwrite itself. - """ - with new_tempdir() as tdir: - tfile = tdir / 'previous-dds.exe' - shutil.copy2(fpath, tfile) - yield tfile - - -@contextmanager -def get_bootstrap_exe(mode: BootstrapMode) -> Iterator[DDSWrapper]: - """Context manager that yields a DDSWrapper around a prior 'dds' executable""" - if mode is BootstrapMode.Lazy: - f = paths.PREBUILT_DDS - if not f.exists(): - _do_bootstrap_download() - elif mode is BootstrapMode.Download: - f = _do_bootstrap_download() - elif mode is BootstrapMode.Build: - f = _do_bootstrap_build() # type: ignore # TODO - elif mode is BootstrapMode.Skip: - f = paths.PREBUILT_DDS - - with pin_exe(f) as dds: - yield DDSWrapper(dds) diff --git a/tools/dds_ci/dds.py b/tools/dds_ci/dds.py deleted file mode 100644 index 6b154e71..00000000 --- a/tools/dds_ci/dds.py +++ /dev/null @@ -1,170 +0,0 @@ -import multiprocessing -import shutil -import os -from pathlib import Path -import copy -from typing import Optional, TypeVar, Iterable - -from . import paths, proc, toolchain as tc_mod -from dds_ci.util import Pathish - -T = TypeVar('T') - - -class DDSWrapper: - """ - Wraps a 'dds' executable with some convenience APIs that invoke various - 'dds' subcommands. - """ - def __init__(self, - path: Path, - *, - repo_dir: Optional[Pathish] = None, - pkg_db_path: Optional[Pathish] = None, - default_cwd: Optional[Pathish] = None) -> None: - self.path = path - self.repo_dir = Path(repo_dir or (paths.PREBUILT_DIR / 'ci-repo')) - self.pkg_db_path = Path(pkg_db_path or (self.repo_dir.parent / 'ci-catalog.db')) - self.default_cwd = default_cwd or Path.cwd() - - def clone(self: T) -> T: - return copy.deepcopy(self) - - @property - def pkg_db_path_arg(self) -> str: - """The arguments for --catalog""" - return f'--catalog={self.pkg_db_path}' - - @property - def cache_dir_arg(self) -> str: - """The arguments for --repo-dir""" - return f'--repo-dir={self.repo_dir}' - - @property - def project_dir_flag(self) -> str: - return '--project-dir' - - def set_repo_scratch(self, path: Pathish) -> None: - self.repo_dir = Path(path) / 'data' - self.pkg_db_path = Path(path) / 'pkgs.db' - - def clean(self, *, build_dir: Optional[Path] = None, repo: bool = True, pkg_db: bool = True) -> None: - """ - Clean out prior executable output, including repos, pkg_db, and - the build results at 'build_dir', if given. - """ - if build_dir and build_dir.exists(): - shutil.rmtree(build_dir) - if repo and self.repo_dir.exists(): - shutil.rmtree(self.repo_dir) - if pkg_db and self.pkg_db_path.exists(): - self.pkg_db_path.unlink() - - def run(self, args: proc.CommandLine, *, cwd: Optional[Pathish] = None, timeout: Optional[int] = None) -> None: - """Execute the 'dds' executable with the given arguments""" - env = os.environ.copy() - env['DDS_NO_ADD_INITIAL_REPO'] = '1' - proc.check_run([self.path, args], cwd=cwd or self.default_cwd, env=env, timeout=timeout) - - def catalog_json_import(self, path: Path) -> None: - """Run 'catalog import' to import the given JSON. Only applicable to older 'dds'""" - self.run(['catalog', 'import', self.pkg_db_path_arg, f'--json={path}']) - - def catalog_get(self, what: str) -> None: - self.run(['catalog', 'get', self.pkg_db_path_arg, what]) - - def pkg_get(self, what: str) -> None: - self.run(['pkg', 'get', self.pkg_db_path_arg, what]) - - def repo_add(self, url: str) -> None: - self.run(['pkg', 'repo', 'add', self.pkg_db_path_arg, url]) - - def repo_remove(self, name: str) -> None: - self.run(['pkg', 'repo', 'remove', self.pkg_db_path_arg, name]) - - def repo_import(self, sdist: Path) -> None: - self.run(['repo', self.cache_dir_arg, 'import', sdist]) - - def pkg_import(self, filepath: Pathish) -> None: - self.run(['pkg', 'import', filepath, self.cache_dir_arg]) - - def build(self, - *, - root: Path, - toolchain: Optional[Path] = None, - build_root: Optional[Path] = None, - jobs: Optional[int] = None, - tweaks_dir: Optional[Path] = None, - more_args: Optional[proc.CommandLine] = None, - timeout: Optional[int] = None) -> None: - """ - Run 'dds build' with the given arguments. - - :param toolchain: The toolchain to use for the build. - :param root: The root project directory. - :param build_root: The root directory where the output will be written. - :param jobs: The number of jobs to use. Default is CPU-count + 2 - """ - toolchain = toolchain or tc_mod.get_default_audit_toolchain() - jobs = jobs or multiprocessing.cpu_count() + 2 - self.run( - [ - 'build', - f'--toolchain={toolchain}', - self.cache_dir_arg, - self.pkg_db_path_arg, - f'--jobs={jobs}', - f'{self.project_dir_flag}={root}', - f'--out={build_root}', - f'--tweaks-dir={tweaks_dir}' if tweaks_dir else (), - more_args or (), - ], - timeout=timeout, - ) - - def compile_file(self, - paths: Iterable[Pathish], - *, - toolchain: Optional[Pathish] = None, - project_dir: Pathish, - out: Optional[Pathish] = None) -> None: - """ - Run 'dds compile-file' for the given paths. - """ - toolchain = toolchain or tc_mod.get_default_audit_toolchain() - self.run([ - 'compile-file', - self.pkg_db_path_arg, - self.cache_dir_arg, - paths, - f'--toolchain={toolchain}', - f'{self.project_dir_flag}={project_dir}', - f'--out={out}', - ]) - - def build_deps(self, args: proc.CommandLine, *, toolchain: Optional[Path] = None) -> None: - toolchain = toolchain or tc_mod.get_default_audit_toolchain() - self.run([ - 'build-deps', - f'--toolchain={toolchain}', - self.pkg_db_path_arg, - self.cache_dir_arg, - args, - ]) - - -class NewDDSWrapper(DDSWrapper): - """ - Wraps the new 'dds' executable with some convenience APIs - """ - @property - def cache_dir_arg(self) -> str: - return f'--pkg-cache-dir={self.repo_dir}' - - @property - def pkg_db_path_arg(self) -> str: - return f'--pkg-db-path={self.pkg_db_path}' - - @property - def project_dir_flag(self) -> str: - return '--project' diff --git a/tools/dds_ci/format.py b/tools/dds_ci/format.py deleted file mode 100644 index 9a6649e0..00000000 --- a/tools/dds_ci/format.py +++ /dev/null @@ -1,70 +0,0 @@ -import argparse -from typing_extensions import Protocol - -import yapf - -from . import paths, proc - - -class FormatArguments(Protocol): - check: bool - cpp: bool - py: bool - - -def start() -> None: - parser = argparse.ArgumentParser() - parser.add_argument('--check', - help='Check whether files need to be formatted, but do not modify them.', - action='store_true') - parser.add_argument('--no-cpp', help='Skip formatting/checking C++ files', action='store_false', dest='cpp') - parser.add_argument('--no-py', help='Skip formatting/checking Python files', action='store_false', dest='py') - args: FormatArguments = parser.parse_args() - - if args.cpp: - format_cpp(args) - if args.py: - format_py(args) - - -def format_cpp(args: FormatArguments) -> None: - src_dir = paths.PROJECT_ROOT / 'src' - cpp_files = src_dir.glob('**/*.[hc]pp') - cf_args: proc.CommandLine = [ - ('--dry-run', '--Werror') if args.check else (), - '-i', # Modify files in-place - '--verbose', - ] - for cf_cand in ('clang-format-10', 'clang-format-9', 'clang-format-8', 'clang-format'): - cf = paths.find_exe(cf_cand) - if not cf: - continue - break - else: - raise RuntimeError('No clang-format executable found') - - print(f'Using clang-format: {cf_cand}') - res = proc.run([cf, cf_args, cpp_files]) - if res.returncode and args.check: - raise RuntimeError('Format checks failed for one or more C++ files. (See above.)') - if res.returncode: - raise RuntimeError('Format execution failed. Check output above.') - - -def format_py(args: FormatArguments) -> None: - py_files = paths.TOOLS_DIR.rglob('*.py') - rc = yapf.main( - list(proc.flatten_cmd([ - '--parallel', - '--verbose', - ('--diff') if args.check else ('--in-place'), - py_files, - ]))) - if rc and args.check: - raise RuntimeError('Format checks for one or more Python files. (See above.)') - if rc: - raise RuntimeError('Format execution failed for Python code. See above.') - - -if __name__ == "__main__": - start() diff --git a/tools/dds_ci/main.py b/tools/dds_ci/main.py deleted file mode 100644 index 5290e6f3..00000000 --- a/tools/dds_ci/main.py +++ /dev/null @@ -1,174 +0,0 @@ -import argparse -import multiprocessing -import pytest -from pathlib import Path -from concurrent import futures -import shutil -import sys -from typing import NoReturn, Sequence, Optional -from typing_extensions import Protocol -import subprocess - -from . import paths, toolchain -from .dds import DDSWrapper -from .bootstrap import BootstrapMode, get_bootstrap_exe - - -def make_argparser() -> argparse.ArgumentParser: - """Create an argument parser for the dds-ci command-line""" - parser = argparse.ArgumentParser() - parser.add_argument('-B', - '--bootstrap-with', - help='How are we to obtain a bootstrapped DDS executable?', - metavar='{download,build,skip,lazy}', - type=BootstrapMode, - default=BootstrapMode.Lazy) - parser.add_argument('--rapid', help='Run CI for fast development iterations', action='store_true') - parser.add_argument('--test-toolchain', - '-TT', - type=Path, - metavar='', - help='The toolchain to use for the first build, which will be passed through the tests') - parser.add_argument('--main-toolchain', - '-T', - type=Path, - dest='toolchain', - metavar='', - help='The toolchain to use for the final build') - parser.add_argument('--jobs', - '-j', - type=int, - help='Number of parallel jobs to use when building and testing', - default=multiprocessing.cpu_count() + 2) - parser.add_argument('--clean', action='store_true', help="Don't remove prior build/deps results") - parser.add_argument('--no-test', - action='store_false', - dest='do_test', - help='Skip testing and just build the final result') - return parser - - -class CommandArguments(Protocol): - """ - The result of parsing argv with the dds-ci argument parser. - """ - #: Whether the user wants us to clean result before building - clean: bool - #: The bootstrap method the user has requested - bootstrap_with: BootstrapMode - #: The toolchain to use when building the 'dds' executable that will be tested. - test_toolchain: Optional[Path] - #: The toolchain to use when building the main 'dds' executable to publish - toolchain: Optional[Path] - #: The maximum number of parallel jobs for build and test - jobs: int - #: Whether we should run the pytest tests - do_test: bool - #: Rapid-CI is for 'dds' development purposes - rapid: bool - - -def parse_argv(argv: Sequence[str]) -> CommandArguments: - """Parse the given dds-ci command-line argument list""" - return make_argparser().parse_args(argv) - - -def test_build(dds: DDSWrapper, args: CommandArguments) -> DDSWrapper: - """ - Execute the build that generates the test-mode executable. Uses the given 'dds' - to build the new dds. Returns a DDSWrapper around the generated test executable. - """ - test_tc = args.test_toolchain or toolchain.get_default_audit_toolchain() - print(f'Test build is building with toolchain: {test_tc}') - build_dir = paths.BUILD_DIR - with toolchain.fixup_toolchain(test_tc) as new_tc: - dds.build(toolchain=new_tc, root=paths.PROJECT_ROOT, build_root=build_dir, jobs=args.jobs, timeout=60 * 15) - return DDSWrapper(build_dir / ('dds' + paths.EXE_SUFFIX)) - - -def run_pytest(dds: DDSWrapper, args: CommandArguments) -> int: - """ - Execute pytest, testing against the given 'test_dds' executable. Returns - the exit code of pytest. - """ - basetemp = Path('/tmp/dds-ci') - basetemp.mkdir(exist_ok=True, parents=True) - return pytest.main([ - '-v', - '--durations=10', - '-n', - str(args.jobs), - f'--basetemp={basetemp}', - f'--dds-exe={dds.path}', - f'--junit-xml={paths.BUILD_DIR}/pytest-junit.xml', - str(paths.PROJECT_ROOT / 'tests/'), - ]) - - -def main_build(dds: DDSWrapper, args: CommandArguments) -> int: - """ - Execute the main build of dds using the given 'dds' executable to build itself. - """ - main_tc = args.toolchain or ( - # If we are in rapid-dev mode, use the test toolchain, which had audit/debug enabled - toolchain.get_default_toolchain() if not args.rapid else toolchain.get_default_audit_toolchain()) - print(f'Building with toolchain: {main_tc}') - with toolchain.fixup_toolchain(main_tc) as new_tc: - try: - dds.build(toolchain=new_tc, - root=paths.PROJECT_ROOT, - build_root=paths.BUILD_DIR, - jobs=args.jobs, - timeout=60 * 15) - except subprocess.CalledProcessError as e: - if args.rapid: - return e.returncode - raise - return 0 - - -def ci_with_dds(dds: DDSWrapper, args: CommandArguments) -> int: - """ - Execute CI using the given prior 'dds' executable. - """ - if args.clean: - dds.clean(build_dir=paths.BUILD_DIR) - - dds.catalog_json_import(paths.PROJECT_ROOT / 'old-catalog.json') - - if args.rapid: - return main_build(dds, args) - - pool = futures.ThreadPoolExecutor() - test_fut = pool.submit(lambda: 0) - if args.do_test: - # Build the test executable: - test_dds = test_build(dds, args) - # Move the generated exe and start tests. We'll start building the main - # EXE and don't want to overwrite the test one while the tests are running - dds_cp = paths.BUILD_DIR / ('dds.test' + paths.EXE_SUFFIX) - test_dds.path.rename(dds_cp) - test_dds.path = dds_cp - # Workaround: dds doesn't rebuild the test-driver on toolchain changes: - shutil.rmtree(paths.BUILD_DIR / '_test-driver') - test_fut = pool.submit(lambda: run_pytest(test_dds, args)) - - main_fut = pool.submit(lambda: main_build(dds, args)) - for fut in futures.as_completed({test_fut, main_fut}): - if fut.result(): - return fut.result() - return 0 - - -def main(argv: Sequence[str]) -> int: - args = parse_argv(argv) - with get_bootstrap_exe(args.bootstrap_with) as f: - return ci_with_dds(f, args) - - -def start() -> NoReturn: - sys.exit(main(sys.argv[1:])) - - -if __name__ == "__main__": - start() diff --git a/tools/dds_ci/proc.py b/tools/dds_ci/proc.py deleted file mode 100644 index fe06d3c1..00000000 --- a/tools/dds_ci/proc.py +++ /dev/null @@ -1,64 +0,0 @@ -from pathlib import PurePath -from typing import Iterable, Union, Optional, Iterator, NoReturn, Sequence, Mapping -from typing_extensions import Protocol -import subprocess - -from .util import Pathish - -CommandLineArg = Union[str, Pathish, int, float] -CommandLineArg1 = Union[CommandLineArg, Iterable[CommandLineArg]] -CommandLineArg2 = Union[CommandLineArg1, Iterable[CommandLineArg1]] -CommandLineArg3 = Union[CommandLineArg2, Iterable[CommandLineArg2]] -CommandLineArg4 = Union[CommandLineArg3, Iterable[CommandLineArg3]] - - -class CommandLine(Protocol): - def __iter__(self) -> Iterator[Union['CommandLine', CommandLineArg]]: - pass - - -# CommandLine = Union[CommandLineArg4, Iterable[CommandLineArg4]] - - -class ProcessResult(Protocol): - args: Sequence[str] - returncode: int - stdout: bytes - stderr: bytes - - -def flatten_cmd(cmd: CommandLine) -> Iterable[str]: - if isinstance(cmd, (str, PurePath)): - yield str(cmd) - elif isinstance(cmd, (int, float)): - yield str(cmd) - elif hasattr(cmd, '__iter__'): - each = (flatten_cmd(arg) for arg in cmd) # type: ignore - for item in each: - yield from item - else: - assert False, f'Invalid command line element: {repr(cmd)}' - - -def run(*cmd: CommandLine, - cwd: Optional[Pathish] = None, - check: bool = False, - env: Optional[Mapping[str, str]] = None, - timeout: Optional[int] = None) -> ProcessResult: - timeout = timeout or 60 * 5 - command = list(flatten_cmd(cmd)) - res = subprocess.run(command, cwd=cwd, check=False, env=env, timeout=timeout) - if res.returncode and check: - raise_error(res) - return res - - -def raise_error(proc: ProcessResult) -> NoReturn: - raise subprocess.CalledProcessError(proc.returncode, proc.args, output=proc.stdout, stderr=proc.stderr) - - -def check_run(*cmd: CommandLine, - cwd: Optional[Pathish] = None, - env: Optional[Mapping[str, str]] = None, - timeout: Optional[int] = None) -> ProcessResult: - return run(cmd, cwd=cwd, check=True, env=env, timeout=timeout) diff --git a/tools/dds_ci/testing/__init__.py b/tools/dds_ci/testing/__init__.py deleted file mode 100644 index cece6680..00000000 --- a/tools/dds_ci/testing/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .fixtures import Project, ProjectOpener, PackageJSON, LibraryJSON -from .http import RepoServer - -__all__ = ( - 'Project', - 'ProjectOpener', - 'PackageJSON', - 'LibraryJSON', - 'RepoServer', -) diff --git a/tools/dds_ci/testing/error.py b/tools/dds_ci/testing/error.py deleted file mode 100644 index 3830a17f..00000000 --- a/tools/dds_ci/testing/error.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -Test utilities for error checking -""" - -from contextlib import contextmanager -from typing import Iterator -import subprocess -from pathlib import Path -import tempfile -import os - - -@contextmanager -def expect_error_marker(expect: str) -> Iterator[None]: - """ - A context-manager function that should wrap a scope that causes an error - from ``dds``. - - :param expect: The error message ID string that is expected to appear. - - The wrapped scope should raise :class:`subprocess.CalledProcessError`. - - After handling the exception, asserts that the subprocess wrote an - error marker containing the string given in ``expect``. - """ - tdir = Path(tempfile.mkdtemp()) - err_file = tdir / 'error' - try: - os.environ['DDS_WRITE_ERROR_MARKER'] = str(err_file) - yield - assert False, 'dds subprocess did not raise CallProcessError' - except subprocess.CalledProcessError: - assert err_file.exists(), \ - f'No error marker file was generated, but dds exited with an error (Expected "{expect}")' - marker = err_file.read_text().strip() - assert marker == expect, \ - f'dds did not produce the expected error (Expected {expect}, got {marker})' - finally: - os.environ.pop('DDS_WRITE_ERROR_MARKER') diff --git a/tools/dds_ci/testing/fixtures.py b/tools/dds_ci/testing/fixtures.py deleted file mode 100644 index f2a9178a..00000000 --- a/tools/dds_ci/testing/fixtures.py +++ /dev/null @@ -1,240 +0,0 @@ -""" -Test fixtures used by DDS in pytest -""" - -from pathlib import Path -import pytest -import json -import shutil -from typing import Sequence, cast, Optional -from typing_extensions import TypedDict - -from _pytest.config import Config as PyTestConfig -from _pytest.tmpdir import TempPathFactory -from _pytest.fixtures import FixtureRequest - -from dds_ci import toolchain, paths -from ..dds import DDSWrapper, NewDDSWrapper -from ..util import Pathish -tc_mod = toolchain - - -def ensure_absent(path: Pathish) -> None: - path = Path(path) - if path.is_dir(): - shutil.rmtree(path) - elif path.exists(): - path.unlink() - else: - # File does not exist, wo we are safe to ignore it - pass - - -class _PackageJSONRequired(TypedDict): - name: str - namespace: str - version: str - - -class PackageJSON(_PackageJSONRequired, total=False): - depends: Sequence[str] - - -class _LibraryJSONRequired(TypedDict): - name: str - - -class LibraryJSON(_LibraryJSONRequired, total=False): - uses: Sequence[str] - - -class Project: - """ - Utilities to access a project being used as a test. - """ - def __init__(self, dirpath: Path, dds: DDSWrapper) -> None: - self.dds = dds.clone() - self.root = dirpath - self.build_root = dirpath / '_build' - - @property - def package_json(self) -> PackageJSON: - """ - Get/set the content of the `package.json` file for the project. - """ - return cast(PackageJSON, json.loads(self.root.joinpath('package.jsonc').read_text())) - - @package_json.setter - def package_json(self, data: PackageJSON) -> None: - self.root.joinpath('package.jsonc').write_text(json.dumps(data, indent=2)) - - @property - def library_json(self) -> LibraryJSON: - """ - Get/set the content of the `library.json` file for the project. - """ - return cast(LibraryJSON, json.loads(self.root.joinpath('library.jsonc').read_text())) - - @library_json.setter - def library_json(self, data: LibraryJSON) -> None: - self.root.joinpath('library.jsonc').write_text(json.dumps(data, indent=2)) - - @property - def project_dir_arg(self) -> str: - """Argument for --project""" - return f'--project={self.root}' - - def build(self, - *, - toolchain: Optional[Pathish] = None, - timeout: Optional[int] = None, - tweaks_dir: Optional[Path] = None) -> None: - """ - Execute 'dds build' on the project - """ - with tc_mod.fixup_toolchain(toolchain or tc_mod.get_default_test_toolchain()) as tc: - self.dds.build(root=self.root, - build_root=self.build_root, - toolchain=tc, - timeout=timeout, - tweaks_dir=tweaks_dir, - more_args=['-ltrace']) - - def compile_file(self, *paths: Pathish, toolchain: Optional[Pathish] = None) -> None: - with tc_mod.fixup_toolchain(toolchain or tc_mod.get_default_test_toolchain()) as tc: - self.dds.compile_file(paths, toolchain=tc, out=self.build_root, project_dir=self.root) - - def pkg_create(self, *, dest: Optional[Pathish] = None) -> None: - self.build_root.mkdir(exist_ok=True, parents=True) - self.dds.run([ - 'pkg', - 'create', - self.project_dir_arg, - f'--out={dest}' if dest else (), - ], cwd=self.build_root) - - def sdist_export(self) -> None: - self.dds.run(['sdist', 'export', self.dds.cache_dir_arg, self.project_dir_arg]) - - def write(self, path: Pathish, content: str) -> Path: - """ - Write the given `content` to `path`. If `path` is relative, it will - be resolved relative to the root directory of this project. - """ - path = Path(path) - if not path.is_absolute(): - path = self.root / path - path.parent.mkdir(exist_ok=True, parents=True) - path.write_text(content) - return path - - -@pytest.fixture() -def test_parent_dir(request: FixtureRequest) -> Path: - """ - :class:`pathlib.Path` fixture pointing to the parent directory of the file - containing the test that is requesting the current fixture - """ - return Path(request.fspath).parent - - -class ProjectOpener(): - """ - A test fixture that opens project directories for testing - """ - def __init__(self, dds: DDSWrapper, request: FixtureRequest, worker: str, - tmp_path_factory: TempPathFactory) -> None: - self.dds = dds - self._request = request - self._worker_id = worker - self._tmppath_fac = tmp_path_factory - - @property - def test_name(self) -> str: - """The name of the test that requested this opener""" - return str(self._request.function.__name__) - - @property - def test_dir(self) -> Path: - """The directory that contains the test that requested this opener""" - return Path(self._request.fspath).parent - - def open(self, dirpath: Pathish) -> Project: - """ - Open a new project testing fixture from the given project directory. - - :param dirpath: The directory that contains the project to use. - - Clones the given directory and then opens a project within that clone. - The clone directory will be destroyed when the test fixture is torn down. - """ - dirpath = Path(dirpath) - if not dirpath.is_absolute(): - dirpath = self.test_dir / dirpath - - proj_copy = self.test_dir / '__test_project' - if self._worker_id != 'master': - proj_copy = self._tmppath_fac.mktemp('test-project-') / self.test_name - else: - self._request.addfinalizer(lambda: ensure_absent(proj_copy)) - - shutil.copytree(dirpath, proj_copy) - new_dds = self.dds.clone() - - if self._worker_id == 'master': - repo_dir = self.test_dir / '__test_repo' - else: - repo_dir = self._tmppath_fac.mktemp('test-repo-') / self.test_name - - new_dds.set_repo_scratch(repo_dir) - new_dds.default_cwd = proj_copy - self._request.addfinalizer(lambda: ensure_absent(repo_dir)) - - return Project(proj_copy, new_dds) - - -@pytest.fixture() -def project_opener(request: FixtureRequest, worker_id: str, dds: DDSWrapper, - tmp_path_factory: TempPathFactory) -> ProjectOpener: - """ - A fixture factory that can open directories as Project objects for building - and testing. Duplicates the project directory into a temporary location so - that the original test directory remains unchanged. - """ - opener = ProjectOpener(dds, request, worker_id, tmp_path_factory) - return opener - - -@pytest.fixture() -def tmp_project(request: FixtureRequest, worker_id: str, project_opener: ProjectOpener, - tmp_path_factory: TempPathFactory) -> Project: - """ - A fixture that generates an empty temporary project directory that will be thrown away - when the test completes. - """ - if worker_id != 'master': - proj_dir = tmp_path_factory.mktemp('temp-project') - return project_opener.open(proj_dir) - - proj_dir = project_opener.test_dir / '__test_project_empty' - ensure_absent(proj_dir) - proj_dir.mkdir() - proj = project_opener.open(proj_dir) - request.addfinalizer(lambda: ensure_absent(proj_dir)) - return proj - - -@pytest.fixture(scope='session') -def dds(dds_exe: Path) -> NewDDSWrapper: - """ - A :class:`~dds_ci.dds.DDSWrapper` around the dds executable under test - """ - wr = NewDDSWrapper(dds_exe) - return wr - - -@pytest.fixture(scope='session') -def dds_exe(pytestconfig: PyTestConfig) -> Path: - """A :class:`pathlib.Path` pointing to the DDS executable under test""" - opt = pytestconfig.getoption('--dds-exe') or paths.BUILD_DIR / 'dds' - return Path(opt) diff --git a/tools/dds_ci/testing/http.py b/tools/dds_ci/testing/http.py deleted file mode 100644 index eeaf7984..00000000 --- a/tools/dds_ci/testing/http.py +++ /dev/null @@ -1,155 +0,0 @@ -from pathlib import Path -import socket -from contextlib import contextmanager, ExitStack, closing -import json -from http.server import SimpleHTTPRequestHandler, HTTPServer -from typing import NamedTuple, Any, Iterator, Callable -from concurrent.futures import ThreadPoolExecutor -from functools import partial -import tempfile -import sys -import subprocess - -import pytest -from _pytest.fixtures import FixtureRequest -from _pytest.tmpdir import TempPathFactory - -from dds_ci.dds import DDSWrapper - - -def _unused_tcp_port() -> int: - """Find an unused localhost TCP port from 1024-65535 and return it.""" - with closing(socket.socket()) as sock: - sock.bind(('127.0.0.1', 0)) - return sock.getsockname()[1] - - -class DirectoryServingHTTPRequestHandler(SimpleHTTPRequestHandler): - """ - A simple HTTP request handler that simply serves files from a directory given to the constructor. - """ - def __init__(self, *args: Any, **kwargs: Any) -> None: - self.dir = kwargs.pop('dir') - super().__init__(*args, **kwargs) - - def translate_path(self, path: str) -> str: - # Convert the given URL path to a path relative to the directory we are serving - abspath = Path(super().translate_path(path)) # type: ignore - relpath = abspath.relative_to(Path.cwd()) - return str(self.dir / relpath) - - -class ServerInfo(NamedTuple): - """ - Information about an HTTP server fixture - """ - base_url: str - root: Path - - -@contextmanager -def run_http_server(dirpath: Path, port: int) -> Iterator[ServerInfo]: - """ - Context manager that spawns an HTTP server that serves thegiven directory on - the given TCP port. - """ - handler = partial(DirectoryServingHTTPRequestHandler, dir=dirpath) - addr = ('127.0.0.1', port) - pool = ThreadPoolExecutor() - with HTTPServer(addr, handler) as httpd: - pool.submit(lambda: httpd.serve_forever(poll_interval=0.1)) - try: - print('Serving at', addr) - yield ServerInfo(f'http://127.0.0.1:{port}', dirpath) - finally: - httpd.shutdown() - - -HTTPServerFactory = Callable[[Path], ServerInfo] - - -@pytest.fixture(scope='session') -def http_server_factory(request: FixtureRequest) -> HTTPServerFactory: - """ - Spawn an HTTP server that serves the content of a directory. - """ - def _make(p: Path) -> ServerInfo: - st = ExitStack() - server = st.enter_context(run_http_server(p, _unused_tcp_port())) - request.addfinalizer(st.pop_all) - return server - - return _make - - -class RepoServer: - """ - A fixture handle to a dds HTTP repository, including a path and URL. - """ - def __init__(self, dds_exe: Path, info: ServerInfo, repo_name: str) -> None: - self.repo_name = repo_name - self.server = info - self.url = info.base_url - self.dds_exe = dds_exe - - def import_json_data(self, data: Any) -> None: - """ - Import some packages into the repo for the given JSON data. Uses - mkrepo.py - """ - with tempfile.NamedTemporaryFile(delete=False) as f: - f.write(json.dumps(data).encode()) - f.close() - self.import_json_file(Path(f.name)) - Path(f.name).unlink() - - def import_json_file(self, fpath: Path) -> None: - """ - Import some package into the repo for the given JSON file. Uses mkrepo.py - """ - subprocess.check_call([ - sys.executable, - str(Path.cwd() / 'tools/mkrepo.py'), - f'--dds-exe={self.dds_exe}', - f'--dir={self.server.root}', - f'--spec={fpath}', - ]) - - -RepoFactory = Callable[[str], Path] - - -@pytest.fixture(scope='session') -def repo_factory(tmp_path_factory: TempPathFactory, dds: DDSWrapper) -> RepoFactory: - def _make(name: str) -> Path: - tmpdir = tmp_path_factory.mktemp('test-repo-') - dds.run(['repoman', 'init', tmpdir, f'--name={name}']) - return tmpdir - - return _make - - -HTTPRepoServerFactory = Callable[[str], RepoServer] - - -@pytest.fixture(scope='session') -def http_repo_factory(dds_exe: Path, repo_factory: RepoFactory, - http_server_factory: HTTPServerFactory) -> HTTPRepoServerFactory: - """ - Fixture factory that creates new repositories with an HTTP server for them. - """ - def _make(name: str) -> RepoServer: - repo_dir = repo_factory(name) - server = http_server_factory(repo_dir) - return RepoServer(dds_exe, server, name) - - return _make - - -@pytest.fixture() -def http_repo(http_repo_factory: HTTPRepoServerFactory, request: FixtureRequest) -> RepoServer: - """ - Fixture that creates a new empty dds repository and an HTTP server to serve - it. - """ - return http_repo_factory(f'test-repo-{request.function.__name__}') diff --git a/tools/dds_ci/util.py b/tools/dds_ci/util.py deleted file mode 100644 index 1a72e2e5..00000000 --- a/tools/dds_ci/util.py +++ /dev/null @@ -1,6 +0,0 @@ -from pathlib import PurePath -from os import PathLike -from typing import Union - -#: A path, string, or convertible-to-Path object -Pathish = Union[PathLike, PurePath, str] diff --git a/tools/docs-watch.sh b/tools/docs-watch.sh deleted file mode 100644 index 3370cf1d..00000000 --- a/tools/docs-watch.sh +++ /dev/null @@ -1,16 +0,0 @@ -set -eu - -THIS_SCRIPT=$(readlink -m $0) -HERE=$(dirname ${THIS_SCRIPT}) -ROOT=$(dirname ${HERE}) - -while true; do - echo "Watching for changes..." - inotifywait -r ${ROOT}/docs/ -q \ - -e modify \ - -e close_write \ - -e move \ - -e delete \ - -e create - make docs || : -done diff --git a/tools/freebsd-gcc-10.jsonc b/tools/freebsd-gcc-10.jsonc index 259900e6..46fb6077 100644 --- a/tools/freebsd-gcc-10.jsonc +++ b/tools/freebsd-gcc-10.jsonc @@ -7,9 +7,14 @@ "warning_flags": [ "-Werror", ], + "cxx_flags": [ + "-fcoroutines", + ], "link_flags": [ "-static-libgcc", - "-static-libstdc++" + "-static-libstdc++", + "-l:libssl.a", + "-l:libcrypto.a", ], "optimize": true } \ No newline at end of file diff --git a/tools/gcc-9-audit-macos.jsonc b/tools/gcc-10-audit-macos.jsonc similarity index 83% rename from tools/gcc-9-audit-macos.jsonc rename to tools/gcc-10-audit-macos.jsonc index 29d8b632..805bdad2 100644 --- a/tools/gcc-9-audit-macos.jsonc +++ b/tools/gcc-10-audit-macos.jsonc @@ -1,20 +1,20 @@ { "$schema": "../res/toolchain-schema.json", "compiler_id": "gnu", - "c_compiler": "gcc-9", - "cxx_compiler": "g++-9", + "c_compiler": "gcc-10", + "cxx_compiler": "g++-10", + "cxx_version": "c++20", "warning_flags": [ "-Werror", ], + "cxx_flags": [ + "-fcoroutines", + ], "flags": [ "-I/usr/local/opt/openssl@1.1/include", /// NOTE: Asan/UBsan misbehave on macOS, so we aren't ready to use them in CI // "-fsanitize=address,undefined", ], - "cxx_flags": [ - "-fconcepts", - "-std=c++2a", - ], "link_flags": [ // "-fsanitize=address,undefined", "/usr/local/opt/openssl@1.1/lib/libssl.a", diff --git a/tools/gcc-9-audit.jsonc b/tools/gcc-10-audit.jsonc similarity index 52% rename from tools/gcc-9-audit.jsonc rename to tools/gcc-10-audit.jsonc index f4699317..673fadd6 100644 --- a/tools/gcc-9-audit.jsonc +++ b/tools/gcc-10-audit.jsonc @@ -1,23 +1,25 @@ { "$schema": "../res/toolchain-schema.json", "compiler_id": "gnu", - "c_compiler": "gcc-9", - "cxx_compiler": "g++-9", + "c_compiler": "gcc-10", + "cxx_compiler": "g++-10", + "cxx_version": "c++20", "warning_flags": [ "-Werror", ], "flags": [ - "-fsanitize=address,undefined", - ], - "cxx_flags": [ - "-fconcepts", - "-std=c++2a", + // "-fsanitize=address,undefined", ], "link_flags": [ - "-fsanitize=address,undefined", + // "-fsanitize=address,undefined", "-l:libssl.a", "-l:libcrypto.a", "-ldl", + // 'eggs' + ], + "cxx_flags": [ + "-fconcepts-diagnostics-depth=10", + "-fcoroutines", ], "debug": true } \ No newline at end of file diff --git a/tools/gcc-9-rel-macos.jsonc b/tools/gcc-10-rel-macos.jsonc similarity index 79% rename from tools/gcc-9-rel-macos.jsonc rename to tools/gcc-10-rel-macos.jsonc index 3ee1a09e..aeeee02a 100644 --- a/tools/gcc-9-rel-macos.jsonc +++ b/tools/gcc-10-rel-macos.jsonc @@ -1,18 +1,18 @@ { "$schema": "../res/toolchain-schema.json", "compiler_id": "gnu", - "c_compiler": "gcc-9", - "cxx_compiler": "g++-9", + "c_compiler": "gcc-10", + "cxx_compiler": "g++-10", + "cxx_version": "c++20", "warning_flags": [ "-Werror", ], + "cxx_flags": [ + "-fcoroutines", + ], "flags": [ "-I/usr/local/opt/openssl@1.1/include", ], - "cxx_flags": [ - "-fconcepts", - "-std=c++2a", - ], "link_flags": [ "-static-libgcc", "-static-libstdc++", diff --git a/tools/gcc-9-rel.jsonc b/tools/gcc-10-rel.jsonc similarity index 74% rename from tools/gcc-9-rel.jsonc rename to tools/gcc-10-rel.jsonc index d43da0a7..02fef990 100644 --- a/tools/gcc-9-rel.jsonc +++ b/tools/gcc-10-rel.jsonc @@ -1,15 +1,15 @@ { "$schema": "../res/toolchain-schema.json", "compiler_id": "gnu", - "c_compiler": "gcc-9", - "cxx_compiler": "g++-9", + "c_compiler": "gcc-10", + "cxx_compiler": "g++-10", + "cxx_version": "c++20", + "cxx_flags": [ + "-fcoroutines", + ], "warning_flags": [ "-Werror", ], - "cxx_flags": [ - "-fconcepts", - "-std=c++2a", - ], "link_flags": [ "-static-libgcc", "-static-libstdc++", diff --git a/tools/gcc-9-static-rel.jsonc b/tools/gcc-10-static-rel.jsonc similarity index 77% rename from tools/gcc-9-static-rel.jsonc rename to tools/gcc-10-static-rel.jsonc index 767471d6..3aa4d7c2 100644 --- a/tools/gcc-9-static-rel.jsonc +++ b/tools/gcc-10-static-rel.jsonc @@ -1,25 +1,27 @@ { "$schema": "../res/toolchain-schema.json", "compiler_id": "gnu", - "c_compiler": "gcc-9", - "cxx_compiler": "g++-9", + "c_compiler": "gcc-10", + "cxx_compiler": "g++-10", + "cxx_version": "c++20", "warning_flags": [ "-Werror", ], + "cxx_flags": [ + "-fcoroutines", + ], "flags": [ "-fdata-sections", "-ffunction-sections", - "-Os" - ], - "cxx_flags": [ - "-fconcepts", - "-std=c++2a" + "-Os", + "-flto", ], "link_flags": [ "-static", "-l:libssl.a", "-l:libcrypto.a", "-ldl", + "-flto=auto", // WORKAROUND: https://sourceware.org/legacy-ml/glibc-bugs/2018-09/msg00009.html "-Wl,-u,pthread_mutex_lock,-u,pthread_mutex_unlock,-u,pthread_self", "-Wl,--gc-sections,--strip-all" diff --git a/tools/gen-catalog-json.py b/tools/gen-catalog-json.py deleted file mode 100644 index cfa6fd2e..00000000 --- a/tools/gen-catalog-json.py +++ /dev/null @@ -1,915 +0,0 @@ -import argparse -import gzip -import os -import json -import json5 -import itertools -import base64 -from urllib import request, parse as url_parse -from typing import NamedTuple, Tuple, List, Sequence, Union, Optional, Mapping, Iterable -import re -from pathlib import Path -import sys -import textwrap -import requests -from threading import local -from concurrent.futures import ThreadPoolExecutor - - -class CopyMoveTransform(NamedTuple): - frm: str - to: str - strip_components: int = 0 - include: Sequence[str] = [] - exclude: Sequence[str] = [] - - def to_dict(self): - return { - 'from': self.frm, - 'to': self.to, - 'include': self.include, - 'exclude': self.exclude, - 'strip-components': self.strip_components, - } - - -class OneEdit(NamedTuple): - kind: str - line: int - content: Optional[str] = None - - def to_dict(self): - d = { - 'kind': self.kind, - 'line': self.line, - } - if self.content: - d['content'] = self.content - return d - - -class EditTransform(NamedTuple): - path: str - edits: Sequence[OneEdit] = [] - - def to_dict(self): - return { - 'path': self.path, - 'edits': [e.to_dict() for e in self.edits], - } - - -class WriteTransform(NamedTuple): - path: str - content: str - - def to_dict(self): - return { - 'path': self.path, - 'content': self.content, - } - - -class RemoveTransform(NamedTuple): - path: str - only_matching: Sequence[str] = () - - def to_dict(self): - return { - 'path': self.path, - 'only-matching': self.only_matching, - } - - -class FSTransform(NamedTuple): - copy: Optional[CopyMoveTransform] = None - move: Optional[CopyMoveTransform] = None - remove: Optional[RemoveTransform] = None - write: Optional[WriteTransform] = None - edit: Optional[EditTransform] = None - - def to_dict(self): - d = {} - if self.copy: - d['copy'] = self.copy.to_dict() - if self.move: - d['move'] = self.move.to_dict() - if self.remove: - d['remove'] = self.remove.to_dict() - if self.write: - d['write'] = self.write.to_dict() - if self.edit: - d['edit'] = self.edit.to_dict() - return d - - -class Git(NamedTuple): - url: str - ref: str - - def to_dict(self) -> dict: - d = { - 'url': self.url, - 'ref': self.ref, - } - return d - - -RemoteInfo = Union[Git] - - -class ForeignInfo(NamedTuple): - remote: RemoteInfo - auto_lib: Optional[str] = None - transforms: Sequence[FSTransform] = [] - - def to_dict(self) -> dict: - d = { - 'transform': [tr.to_dict() for tr in self.transforms], - } - if isinstance(self.remote, Git): - d['git'] = self.remote.to_dict() - if self.auto_lib: - d['auto-lib'] = self.auto_lib - return d - - -class Version(NamedTuple): - version: str - remote: ForeignInfo - depends: Sequence[str] = [] - description: str = '(No description provided)' - - def to_dict(self) -> dict: - ret: dict = { - 'description': self.description, - 'depends': list(self.depends), - 'remote': self.remote.to_dict(), - } - return ret - - -class VersionSet(NamedTuple): - version: str - depends: Sequence[str] - - -class Package(NamedTuple): - name: str - versions: List[Version] - - -HTTP_POOL = ThreadPoolExecutor(10) - -HTTP_SESSION = requests.Session() - - -def github_http_get(url: str): - url_dat = url_parse.urlparse(url) - req = request.Request(url) - req.add_header('Accept-Encoding', 'application/json') - req.add_header('Authorization', f'token {os.environ["GITHUB_API_TOKEN"]}') - if url_dat.hostname != 'api.github.com': - raise RuntimeError(f'Request is outside of api.github.com [{url}]') - print(f'Request {url}') - resp = HTTP_SESSION.get(url, headers=req.headers) - # resp = request.urlopen(req) - resp.raise_for_status() - # if resp.status != 200: - # raise RuntimeError(f'Request to [{url}] failed [{resp.status} {resp.reason}]') - return json5.loads(resp.text) - - -def _get_github_tree_file_content(url: str) -> bytes: - json_body = github_http_get(url) - content_b64 = json_body['content'].encode() - assert json_body['encoding'] == 'base64', json_body - content = base64.decodebytes(content_b64) - return content - - -def _version_for_github_tag(pkg_name: str, desc: str, clone_url: str, tag) -> Version: - print(f'Loading tag {tag["name"]}') - commit = github_http_get(tag['commit']['url']) - tree = github_http_get(commit['commit']['tree']['url']) - - tree_content = {t['path']: t for t in tree['tree']} - cands = ['package.json', 'package.jsonc', 'package.json5'] - for cand in cands: - if cand in tree_content: - package_json_fname = cand - break - else: - raise RuntimeError(f'No package JSON5 file in tag {tag["name"]} for {pkg_name} (One of {tree_content.keys()})') - - package_json = json5.loads(_get_github_tree_file_content(tree_content[package_json_fname]['url'])) - version = package_json['version'] - if pkg_name != package_json['name']: - raise RuntimeError(f'package name in repo "{package_json["name"]}" ' - f'does not match expected name "{pkg_name}"') - - depends = package_json.get('depends') - pairs: Iterable[str] - if isinstance(depends, dict): - pairs = ((k + v) for k, v in depends.items()) - elif isinstance(depends, list): - pairs = depends - elif depends is None: - pairs = [] - else: - raise RuntimeError(f'Unknown "depends" object from json file: {depends!r}') - - remote = Git(url=clone_url, ref=tag['name']) - return Version(version, description=desc, depends=list(pairs), remote=ForeignInfo(remote)) - - -def github_package(name: str, repo: str, want_tags: Iterable[str]) -> Package: - print(f'Downloading repo from {repo}') - repo_data = github_http_get(f'https://api.github.com/repos/{repo}') - desc = repo_data['description'] - avail_tags = github_http_get(repo_data['tags_url']) - - missing_tags = set(want_tags) - set(t['name'] for t in avail_tags) - if missing_tags: - raise RuntimeError('One or more wanted tags do not exist in ' - f'the repository "{repo}" (Missing: {missing_tags})') - - tag_items = (t for t in avail_tags if t['name'] in want_tags) - - versions = HTTP_POOL.map(lambda tag: _version_for_github_tag(name, desc, repo_data['clone_url'], tag), tag_items) - - return Package(name, list(versions)) - - -def simple_packages(name: str, - description: str, - git_url: str, - versions: Sequence[VersionSet], - auto_lib: Optional[str] = None, - *, - tag_fmt: str = '{}') -> Package: - return Package(name, [ - Version( - ver.version, - description=description, - remote=ForeignInfo(remote=Git(git_url, tag_fmt.format(ver.version)), auto_lib=auto_lib), - depends=ver.depends) for ver in versions - ]) - - -def many_versions(name: str, - versions: Sequence[str], - *, - tag_fmt: str = '{}', - git_url: str, - auto_lib: str = None, - transforms: Sequence[FSTransform] = (), - description='(No description was provided)') -> Package: - return Package(name, [ - Version( - ver, - description='\n'.join(textwrap.wrap(description)), - remote=ForeignInfo( - remote=Git(url=git_url, ref=tag_fmt.format(ver)), auto_lib=auto_lib, transforms=transforms)) - for ver in versions - ]) - - -# yapf: disable -PACKAGES = [ - github_package('neo-buffer', 'vector-of-bool/neo-buffer', ['0.2.1', '0.3.0', '0.4.0', '0.4.1', '0.4.2']), - github_package('neo-compress', 'vector-of-bool/neo-compress', ['0.1.0', '0.1.1', '0.2.0']), - github_package('neo-url', 'vector-of-bool/neo-url', ['0.1.0', '0.1.1', '0.1.2', '0.2.0', '0.2.1', '0.2.2']), - github_package('neo-sqlite3', 'vector-of-bool/neo-sqlite3', ['0.2.3', '0.3.0', '0.4.0', '0.4.1']), - github_package('neo-fun', 'vector-of-bool/neo-fun', [ - '0.1.1', - '0.2.0', - '0.2.1', - '0.3.0', - '0.3.1', - '0.3.2', - '0.4.0', - '0.4.1', - '0.4.2', - '0.5.0', - '0.5.1', - '0.5.2', - '0.5.3', - '0.5.4', - '0.5.5', - '0.6.0', - ]), - github_package('neo-io', 'vector-of-bool/neo-io', ['0.1.0', '0.1.1']), - github_package('neo-http', 'vector-of-bool/neo-http', ['0.1.0']), - github_package('neo-concepts', 'vector-of-bool/neo-concepts', ( - '0.2.2', - '0.3.0', - '0.3.1', - '0.3.2', - '0.4.0', - )), - github_package('semver', 'vector-of-bool/semver', ['0.2.2']), - github_package('pubgrub', 'vector-of-bool/pubgrub', ['0.2.1']), - github_package('vob-json5', 'vector-of-bool/json5', ['0.1.5']), - github_package('vob-semester', 'vector-of-bool/semester', ['0.1.0', '0.1.1', '0.2.0', '0.2.1', '0.2.2']), - many_versions( - 'magic_enum', - ( - '0.5.0', - '0.6.0', - '0.6.1', - '0.6.2', - '0.6.3', - '0.6.4', - '0.6.5', - '0.6.6', - ), - description='Static reflection for enums', - tag_fmt='v{}', - git_url='https://github.com/Neargye/magic_enum.git', - auto_lib='neargye/magic_enum', - ), - many_versions( - 'nameof', - [ - '0.8.3', - '0.9.0', - '0.9.1', - '0.9.2', - '0.9.3', - '0.9.4', - ], - description='Nameof operator for modern C++', - tag_fmt='v{}', - git_url='https://github.com/Neargye/nameof.git', - auto_lib='neargye/nameof', - ), - many_versions( - 'range-v3', - ( - '0.5.0', - '0.9.0', - '0.9.1', - '0.10.0', - '0.11.0', - ), - git_url='https://github.com/ericniebler/range-v3.git', - auto_lib='range-v3/range-v3', - description='Range library for C++14/17/20, basis for C++20\'s std::ranges', - ), - many_versions( - 'nlohmann-json', - ( - # '3.0.0', - # '3.0.1', - # '3.1.0', - # '3.1.1', - # '3.1.2', - # '3.2.0', - # '3.3.0', - # '3.4.0', - # '3.5.0', - # '3.6.0', - # '3.6.1', - # '3.7.0', - '3.7.1', # Only this version has the dds forked branch - # '3.7.2', - # '3.7.3', - ), - git_url='https://github.com/vector-of-bool/json.git', - tag_fmt='dds/{}', - description='JSON for Modern C++', - ), - Package('ms-wil', [ - Version( - '2020.3.16', - description='The Windows Implementation Library', - remote=ForeignInfo(Git('https://github.com/vector-of-bool/wil.git', 'dds/2020.03.16'))) - ]), - Package('p-ranav.argparse', [ - Version( - '2.1.0', - description='Argument Parser for Modern C++', - remote=ForeignInfo(Git('https://github.com/p-ranav/argparse.git', 'v2.1'), auto_lib='p-ranav/argparse')) - ]), - many_versions( - 'ctre', - ( - '2.8.1', - '2.8.2', - '2.8.3', - '2.8.4', - ), - git_url='https://github.com/hanickadot/compile-time-regular-expressions.git', - tag_fmt='v{}', - auto_lib='hanickadot/ctre', - description='A compile-time PCRE (almost) compatible regular expression matcher', - ), - Package( - 'spdlog', - [ - Version( - ver, - description='Fast C++ logging library', - depends=['fmt+6.0.0'], - remote=ForeignInfo( - Git(url='https://github.com/gabime/spdlog.git', ref=f'v{ver}'), - transforms=[ - FSTransform( - write=WriteTransform( - path='package.json', - content=json.dumps({ - 'name': 'spdlog', - 'namespace': 'spdlog', - 'version': ver, - 'depends': ['fmt+6.0.0'], - }))), - FSTransform( - write=WriteTransform( - path='library.json', content=json.dumps({ - 'name': 'spdlog', - 'uses': ['fmt/fmt'] - }))), - FSTransform( - # It's all just template instantiations. - remove=RemoveTransform(path='src/'), - # Tell spdlog to use the external fmt library - edit=EditTransform( - path='include/spdlog/tweakme.h', - edits=[ - OneEdit( - kind='insert', - content='#define SPDLOG_FMT_EXTERNAL 1', - line=13, - ), - ])), - ], - ), - ) for ver in ( - '1.4.0', - '1.4.1', - '1.4.2', - '1.5.0', - '1.6.0', - '1.6.1', - '1.7.0', - ) - ]), - many_versions( - 'fmt', - ( - '6.0.0', - '6.1.0', - '6.1.1', - '6.1.2', - '6.2.0', - '6.2.1', - '7.0.0', - '7.0.1', - '7.0.2', - '7.0.3', - ), - git_url='https://github.com/fmtlib/fmt.git', - auto_lib='fmt/fmt', - description='A modern formatting library : https://fmt.dev/', - ), - Package('catch2', [ - Version( - '2.12.4', - description='A modern C++ unit testing library', - remote=ForeignInfo( - Git('https://github.com/catchorg/Catch2.git', 'v2.12.4'), - auto_lib='catch2/catch2', - transforms=[ - FSTransform(move=CopyMoveTransform(frm='include', to='include/catch2')), - FSTransform( - copy=CopyMoveTransform(frm='include', to='src'), - write=WriteTransform( - path='include/catch2/catch_with_main.hpp', - content=''' - #pragma once - - #define CATCH_CONFIG_MAIN - #include "./catch.hpp" - - namespace Catch { - - CATCH_REGISTER_REPORTER("console", ConsoleReporter) - - } - ''')), - ])) - ]), - Package('asio', [ - Version( - ver, - description='Asio asynchronous I/O C++ library', - remote=ForeignInfo( - Git('https://github.com/chriskohlhoff/asio.git', f'asio-{ver.replace(".", "-")}'), - auto_lib='asio/asio', - transforms=[ - FSTransform( - move=CopyMoveTransform( - frm='asio/src', - to='src/', - ), - remove=RemoveTransform( - path='src/', - only_matching=[ - 'doc/**', - 'examples/**', - 'tests/**', - 'tools/**', - ], - ), - ), - FSTransform( - move=CopyMoveTransform( - frm='asio/include/', - to='include/', - ), - edit=EditTransform( - path='include/asio/detail/config.hpp', - edits=[ - OneEdit(line=13, kind='insert', content='#define ASIO_STANDALONE 1'), - OneEdit(line=14, kind='insert', content='#define ASIO_SEPARATE_COMPILATION 1') - ]), - ), - ]), - ) for ver in [ - '1.12.0', - '1.12.1', - '1.12.2', - '1.13.0', - '1.14.0', - '1.14.1', - '1.16.0', - '1.16.1', - ] - ]), - Package( - 'abseil', - [ - Version( - ver, - description='Abseil Common Libraries', - remote=ForeignInfo( - Git('https://github.com/abseil/abseil-cpp.git', tag), - auto_lib='abseil/abseil', - transforms=[ - FSTransform( - move=CopyMoveTransform( - frm='absl', - to='src/absl/', - ), - remove=RemoveTransform( - path='src/', - only_matching=[ - '**/*_test.c*', - '**/*_testing.c*', - '**/*_benchmark.c*', - '**/benchmarks.c*', - '**/*_test_common.c*', - '**/mocking_*.c*', - # Misc files that should be removed: - '**/test_util.cc', - '**/mutex_nonprod.cc', - '**/named_generator.cc', - '**/print_hash_of.cc', - '**/*_gentables.cc', - ]), - ) - ]), - ) for ver, tag in [ - ('2018.6.0', '20180600'), - ('2019.8.8', '20190808'), - ('2020.2.25', '20200225.2'), - ] - ]), - Package('zlib', [ - Version( - ver, - description='A massively spiffy yet delicately unobtrusive compression library', - remote=ForeignInfo( - Git('https://github.com/madler/zlib.git', tag or f'v{ver}'), - auto_lib='zlib/zlib', - transforms=[ - FSTransform(move=CopyMoveTransform( - frm='.', - to='src/', - include=[ - '*.c', - '*.h', - ], - )), - FSTransform(move=CopyMoveTransform( - frm='src/', - to='include/', - include=['zlib.h', 'zconf.h'], - )), - ]), - ) for ver, tag in [ - ('1.2.11', None), - ('1.2.10', None), - ('1.2.9', None), - ('1.2.8', None), - ('1.2.7', 'v1.2.7.3'), - ('1.2.6', 'v1.2.6.1'), - ('1.2.5', 'v1.2.5.3'), - ('1.2.4', 'v1.2.4.5'), - ('1.2.3', 'v1.2.3.8'), - ('1.2.2', 'v1.2.2.4'), - ('1.2.1', 'v1.2.1.2'), - ('1.2.0', 'v1.2.0.8'), - ] - ]), - Package('sol2', [ - Version( - ver, - description='A C++ <-> Lua API wrapper with advanced features and top notch performance', - depends=['lua+0.0.0'], - remote=ForeignInfo( - Git('https://github.com/ThePhD/sol2.git', f'v{ver}'), - transforms=[ - FSTransform( - write=WriteTransform( - path='package.json', - content=json.dumps( - { - 'name': 'sol2', - 'namespace': 'sol2', - 'version': ver, - 'depends': [f'lua+0.0.0'], - }, - indent=2, - )), - move=(None if ver.startswith('3.') else CopyMoveTransform( - frm='sol', - to='src/sol', - )), - ), - FSTransform( - write=WriteTransform( - path='library.json', - content=json.dumps( - { - 'name': 'sol2', - 'uses': ['lua/lua'], - }, - indent=2, - ))), - ]), - ) for ver in [ - '3.2.1', - '3.2.0', - '3.0.3', - '3.0.2', - '2.20.6', - '2.20.5', - '2.20.4', - '2.20.3', - '2.20.2', - '2.20.1', - '2.20.0', - ] - ]), - Package('lua', [ - Version( - ver, - description= - 'Lua is a powerful and fast programming language that is easy to learn and use and to embed into your application.', - remote=ForeignInfo( - Git('https://github.com/lua/lua.git', f'v{ver}'), - auto_lib='lua/lua', - transforms=[FSTransform(move=CopyMoveTransform( - frm='.', - to='src/', - include=['*.c', '*.h'], - ))]), - ) for ver in [ - '5.4.0', - '5.3.5', - '5.3.4', - '5.3.3', - '5.3.2', - '5.3.1', - '5.3.0', - '5.2.3', - '5.2.2', - '5.2.1', - '5.2.0', - '5.1.1', - ] - ]), - Package('pegtl', [ - Version( - ver, - description='Parsing Expression Grammar Template Library', - remote=ForeignInfo( - Git('https://github.com/taocpp/PEGTL.git', ver), - auto_lib='tao/pegtl', - transforms=[FSTransform(remove=RemoveTransform(path='src/'))], - )) for ver in [ - '2.8.3', - '2.8.2', - '2.8.1', - '2.8.0', - '2.7.1', - '2.7.0', - '2.6.1', - '2.6.0', - ] - ]), - many_versions( - 'boost.pfr', ['1.0.0', '1.0.1'], auto_lib='boost/pfr', git_url='https://github.com/apolukhin/magic_get.git'), - many_versions( - 'boost.leaf', - [ - '0.1.0', - '0.2.0', - '0.2.1', - '0.2.2', - '0.2.3', - '0.2.4', - '0.2.5', - '0.3.0', - ], - auto_lib='boost/leaf', - git_url='https://github.com/zajo/leaf.git', - ), - many_versions( - 'boost.mp11', - ['1.70.0', '1.71.0', '1.72.0', '1.73.0'], - tag_fmt='boost-{}', - git_url='https://github.com/boostorg/mp11.git', - auto_lib='boost/mp11', - ), - many_versions( - 'libsodium', [ - '1.0.10', - '1.0.11', - '1.0.12', - '1.0.13', - '1.0.14', - '1.0.15', - '1.0.16', - '1.0.17', - '1.0.18', - ], - git_url='https://github.com/jedisct1/libsodium.git', - auto_lib='sodium/sodium', - description='Sodium is a new, easy-to-use software library ' - 'for encryption, decryption, signatures, password hashing and more.', - transforms=[ - FSTransform( - move=CopyMoveTransform(frm='src/libsodium/include', to='include/'), - edit=EditTransform( - path='include/sodium/export.h', - edits=[OneEdit(line=8, kind='insert', content='#define SODIUM_STATIC 1')])), - FSTransform( - edit=EditTransform( - path='include/sodium/private/common.h', - edits=[ - OneEdit( - kind='insert', - line=1, - content=Path(__file__).parent.joinpath('libsodium-config.h').read_text(), - ) - ])), - FSTransform( - copy=CopyMoveTransform( - frm='builds/msvc/version.h', - to='include/sodium/version.h', - ), - move=CopyMoveTransform( - frm='src/libsodium', - to='src/', - ), - remove=RemoveTransform(path='src/libsodium'), - ), - FSTransform(copy=CopyMoveTransform(frm='include', to='src/', strip_components=1)), - ]), - many_versions( - 'tomlpp', - [ - '1.0.0', - '1.1.0', - '1.2.0', - '1.2.3', - '1.2.4', - '1.2.5', - '1.3.0', - # '1.3.2', # Wrong tag name in upstream - '1.3.3', - '2.0.0', - ], - tag_fmt='v{}', - git_url='https://github.com/marzer/tomlplusplus.git', - auto_lib='tomlpp/tomlpp', - description='Header-only TOML config file parser and serializer for modern C++'), - Package('inja', [ - *(Version( - ver, - description='A Template Engine for Modern C++', - remote=ForeignInfo(Git('https://github.com/pantor/inja.git', f'v{ver}'), auto_lib='inja/inja')) - for ver in ('1.0.0', '2.0.0', '2.0.1')), - *(Version( - ver, - description='A Template Engine for Modern C++', - depends=['nlohmann-json+0.0.0'], - remote=ForeignInfo( - Git('https://github.com/pantor/inja.git', f'v{ver}'), - transforms=[ - FSTransform( - write=WriteTransform( - path='package.json', - content=json.dumps({ - 'name': 'inja', - 'namespace': 'inja', - 'version': ver, - 'depends': [ - 'nlohmann-json+0.0.0', - ] - }))), - FSTransform( - write=WriteTransform( - path='library.json', content=json.dumps({ - 'name': 'inja', - 'uses': ['nlohmann/json'] - }))), - ], - auto_lib='inja/inja', - )) for ver in ('2.1.0', '2.2.0')), - ]), - many_versions( - 'cereal', - [ - '0.9.0', - '0.9.1', - '1.0.0', - '1.1.0', - '1.1.1', - '1.1.2', - '1.2.0', - '1.2.1', - '1.2.2', - '1.3.0', - ], - auto_lib='cereal/cereal', - git_url='https://github.com/USCiLab/cereal.git', - tag_fmt='v{}', - description='A C++11 library for serialization', - ), - many_versions( - 'pybind11', - [ - '2.0.0', - '2.0.1', - '2.1.0', - '2.1.1', - '2.2.0', - '2.2.1', - '2.2.2', - '2.2.3', - '2.2.4', - '2.3.0', - '2.4.0', - '2.4.1', - '2.4.2', - '2.4.3', - '2.5.0', - ], - git_url='https://github.com/pybind/pybind11.git', - description='Seamless operability between C++11 and Python', - auto_lib='pybind/pybind11', - tag_fmt='v{}', - ), - Package('pcg-cpp', [ - Version( - '0.98.1', - description='PCG Randum Number Generation, C++ Edition', - remote=ForeignInfo(Git(url='https://github.com/imneme/pcg-cpp.git', ref='v0.98.1'), auto_lib='pcg/pcg-cpp')) - ]), - many_versions( - 'hinnant-date', - ['2.4.1', '3.0.0'], - description='A date and time library based on the C++11/14/17 header', - auto_lib='hinnant/date', - git_url='https://github.com/HowardHinnant/date.git', - tag_fmt='v{}', - transforms=[FSTransform(remove=RemoveTransform(path='src/'))], - ), -] -# yapf: enable - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - args = parser.parse_args() - - data = { - 'version': 1, - 'packages': {pkg.name: {ver.version: ver.to_dict() - for ver in pkg.versions} - for pkg in PACKAGES} - } - Path('catalog.json').write_text(json.dumps(data, indent=2, sort_keys=True)) diff --git a/tools/gen-spdx.py b/tools/gen-spdx.py new file mode 100644 index 00000000..abcb3d0b --- /dev/null +++ b/tools/gen-spdx.py @@ -0,0 +1,42 @@ +from typing import Any +from operator import itemgetter +from pathlib import Path +import urllib.request +import json +from string import Template + + +def http_get(url: str) -> bytes: + return urllib.request.urlopen(url).read() + + +print('Downloading SPDX licenses') +licenses = json.loads(http_get('https://github.com/spdx/license-list-data/raw/master/json/licenses.json')) +exceptions = json.loads(http_get('https://github.com/spdx/license-list-data/raw/master/json/exceptions.json')) + +ITEM_TEMPLATE = Template(r'''SPDX_LICENSE($id, $name)''') + + +def quote(s: str) -> str: + return f'R"_({s})_"' + + +def render_license(dat: Any) -> str: + id_ = dat['licenseId'] + name = dat['name'] + return f'SPDX_LICENSE({quote(id_)}, {quote(name)})' + + +def render_exception(dat: Any) -> str: + id_ = dat['licenseExceptionId'] + name = dat['name'] + return f'SPDX_EXCEPTION({quote(id_)}, {quote(name)})' + + +lic_lines = (render_license(l) for l in sorted(licenses['licenses'], key=itemgetter('licenseId'))) +exc_lines = (render_exception(e) for e in sorted(exceptions['exceptions'], key=itemgetter('licenseExceptionId'))) + +proj_dir = Path(__file__).resolve().parent.parent / 'src/bpt/project' +print('Writing license content') +proj_dir.joinpath('spdx.inl').write_text('\n'.join(lic_lines), encoding='utf-8') +proj_dir.joinpath('spdx-exc.inl').write_text('\n'.join(exc_lines), encoding='utf-8') diff --git a/tools/get-win-openssl.ps1 b/tools/get-win-openssl.ps1 index 09a3c2c0..5267af74 100644 --- a/tools/get-win-openssl.ps1 +++ b/tools/get-win-openssl.ps1 @@ -9,16 +9,7 @@ $root_dir = Split-Path -Parent $tools_dir $build_dir = Join-Path $root_dir "_build" New-Item -ItemType Container $build_dir -ErrorAction Ignore -$local_tgz = Join-Path $build_dir "openssl.tgz" - -# This is the path to the release static vs2019 x64 build of OpenSSL in bintray -$conan_ssl_path = "_/openssl/1.1.1h/_/7098aea4e4f2247cc9b5dcaaa1ebddbe/package/a79a557254fabcb77849dd623fed97c9c5ab7651/141ef2c6711a254707ba1f7f4fd07ad4" -$openssl_url = "https://dl.bintray.com/conan/conan-center/$conan_ssl_path/conan_package.tgz" - -Write-Host "Downloading OpenSSL for Windows" -Invoke-WebRequest ` - -Uri $openssl_url ` - -OutFile $local_tgz +$local_tgz = Join-Path $tools_dir "windows-openssl.tgz" $openssl_tree = Join-Path $root_dir "external/OpenSSL" Write-Host "Expanding OpenSSL archive..." diff --git a/tools/libsodium-config.h b/tools/libsodium-config.h deleted file mode 100644 index e9519a1a..00000000 --- a/tools/libsodium-config.h +++ /dev/null @@ -1,141 +0,0 @@ -#pragma once - -// clang-format off - -/** - * Header checks - */ -#if __has_include() - #define HAVE_SYS_MMAN_H 1 -#endif - -#if __has_include() - #define HAVE_SYS_RANDOM_H 1 -#endif - -#if __has_include() - #define HAVE_INTRIN_H 1 -#endif - -#if __has_include() - #define HAVE_SYS_AUXV_H 1 -#endif - -/** - * Architectural checks for intrinsics - */ -#if __has_include() && __MMX__ - #define HAVE_MMINTRIN_H 1 -#endif - -#if __has_include() && __SSE2__ - #define HAVE_EMMINTRIN_H 1 -#endif - -#if __SSE3__ - #if __has_include() - #define HAVE_PMMINTRIN_H 1 - #endif - #if __has_include() - #define HAVE_TMMINTRIN_H 1 - #endif -#endif - -#if __has_include() && __SSE4_1__ - #define HAVE_SMMINTRIN_H -#endif - -#if __has_include() - #if __AVX__ - #define HAVE_AVXINTRIN_H - #endif - #if __AVX2__ - #define HAVE_AVX2INTRIN_H - #endif - #if __AVX512F__ - #if defined(__clang__) && __clang_major__ < 4 - // AVX512 may be broken - #elif defined(__GNUC__) && __GNUC__ < 6 - // '' - #else - #define HAVE_AVX512FINTRIN_H - #endif - #endif -#endif - -#if __has_include() && __AES__ - #define HAVE_WMMINTRIN_H 1 -#endif - -#if __RDRND__ - #define HAVE_RDRAND -#endif - -/** - * Detect mman APIs - */ -#if __has_include() - #define HAVE_MMAP 1 - #define HAVE_MPROTECT 1 - #define HAVE_MLOCK 1 - - #if defined(_DEFAULT_SOURCE) || defined(_BSD_SOURCE) - #define HAVE_MADVISE 1 - #endif -#endif - -#if __has_include() - #define HAVE_GETRANDOM 1 -#endif - -/** - * POSIX-Only stuff - */ -#if __has_include() - #if defined(_DEFAULT_SOURCE) - #define HAVE_GETENTROPY 1 - #endif - - /** - * Default POSIX APIs - */ - #define HAVE_POSIX_MEMALIGN 1 - #define HAVE_GETPID 1 - #define HAVE_NANOSLEEP 1 - - /** - * Language/library features from C11 - */ - #if __STDC_VERSION__ >= 201112L - #define HAVE_MEMSET_S 1 - #endif - - #if __linux__ - #define HAVE_EXPLICIT_BZERO 1 - #endif -#endif - -/** - * Miscellaneous - */ -#if __has_include() - #define HAVE_PTHREAD 1 -#endif - -#if __has_include() - #include - #if __BYTE_ORDER == __BIG_ENDIAN - #define NATIVE_BIG_ENDIAN 1 - #elif __BYTE_ORDER == __LITTLE_ENDIAN - #define NATIVE_LITTLE_ENDIAN 1 - #else - #error "Unknown endianness for this platform." - #endif -#elif defined(_MSVC) - // At time of writing, MSVC only targets little-endian. - #define NATIVE_LITTLE_ENDIAN 1 -#else - #error "Unknown endianness for this platform." -#endif - -#define CONFIGURED 1 diff --git a/tools/mkrepo.py b/tools/mkrepo.py deleted file mode 100644 index dac00023..00000000 --- a/tools/mkrepo.py +++ /dev/null @@ -1,449 +0,0 @@ -""" -Script for populating a repository with packages declaratively. -""" - -import argparse -import itertools -import json -import os -import re -import shutil -import stat -import sys -import tarfile -import tempfile -from concurrent.futures import ThreadPoolExecutor -from contextlib import contextmanager -from pathlib import Path -from subprocess import check_call -from threading import Lock -from typing import (Any, Dict, Iterable, Iterator, NamedTuple, NoReturn, Optional, Sequence, Tuple, Type, TypeVar, - Union) -from urllib import request - -from semver import VersionInfo -from typing_extensions import Protocol - -T = TypeVar('T') - -I32_MAX = 0xffff_ffff - 1 -MAX_VERSION = VersionInfo(I32_MAX, I32_MAX, I32_MAX) - -REPO_ROOT = Path(__file__).resolve().absolute().parent.parent - - -def _get_dds_exe() -> Path: - suffix = '.exe' if os.name == 'nt' else '' - dirs = [REPO_ROOT / '_build', REPO_ROOT / '_prebuilt'] - for d in dirs: - exe = d / ('dds' + suffix) - if exe.is_file(): - return exe - raise RuntimeError('Unable to find a dds.exe to use') - - -class Dependency(NamedTuple): - name: str - low: VersionInfo - high: VersionInfo - - @classmethod - def parse(cls: Type[T], depstr: str) -> T: - mat = re.match(r'(.+?)([\^~\+@])(.+?)$', depstr) - if not mat: - raise ValueError(f'Invalid dependency string "{depstr}"') - name, kind, version_str = mat.groups() - version = VersionInfo.parse(version_str) - high = { - '^': version.bump_major, - '~': version.bump_minor, - '@': version.bump_patch, - '+': lambda: MAX_VERSION, - }[kind]() - return cls(name, version, high) - - -def glob_if_exists(path: Path, pat: str) -> Iterable[Path]: - try: - yield from path.glob(pat) - except FileNotFoundError: - yield from () - - -class MoveTransform(NamedTuple): - frm: str - to: str - strip_components: int = 0 - include: Sequence[str] = [] - exclude: Sequence[str] = [] - - @classmethod - def parse_data(cls: Type[T], data: Any) -> T: - return cls(frm=data.pop('from'), - to=data.pop('to'), - include=data.pop('include', []), - strip_components=data.pop('strip-components', 0), - exclude=data.pop('exclude', [])) - - def apply_to(self, p: Path) -> None: - src = p / self.frm - dest = p / self.to - if src.is_file(): - self.do_reloc_file(src, dest) - return - - inc_pats = self.include or ['**/*'] - include = set(itertools.chain.from_iterable(glob_if_exists(src, pat) for pat in inc_pats)) - exclude = set(itertools.chain.from_iterable(glob_if_exists(src, pat) for pat in self.exclude)) - to_reloc = sorted(include - exclude) - for source_file in to_reloc: - relpath = source_file.relative_to(src) - strip_relpath = Path('/'.join(relpath.parts[self.strip_components:])) - dest_file = dest / strip_relpath - self.do_reloc_file(source_file, dest_file) - - def do_reloc_file(self, src: Path, dest: Path) -> None: - if src.is_dir(): - dest.mkdir(exist_ok=True, parents=True) - else: - dest.parent.mkdir(exist_ok=True, parents=True) - src.rename(dest) - - -class CopyTransform(MoveTransform): - def do_reloc_file(self, src: Path, dest: Path) -> None: - if src.is_dir(): - dest.mkdir(exist_ok=True, parents=True) - else: - shutil.copy2(src, dest) - - -class OneEdit(NamedTuple): - kind: str - line: int - content: Optional[str] = None - - @classmethod - def parse_data(cls, data: Dict) -> 'OneEdit': - return OneEdit(data.pop('kind'), data.pop('line'), data.pop('content', None)) - - def apply_to(self, fpath: Path) -> None: - fn = { - 'insert': self._insert, - # 'delete': self._delete, - }[self.kind] - fn(fpath) - - def _insert(self, fpath: Path) -> None: - content = fpath.read_bytes() - lines = content.split(b'\n') - assert self.content - lines.insert(self.line, self.content.encode()) - fpath.write_bytes(b'\n'.join(lines)) - - -class EditTransform(NamedTuple): - path: str - edits: Sequence[OneEdit] = [] - - @classmethod - def parse_data(cls, data: Dict) -> 'EditTransform': - return EditTransform(data.pop('path'), [OneEdit.parse_data(ed) for ed in data.pop('edits')]) - - def apply_to(self, p: Path) -> None: - fpath = p / self.path - for ed in self.edits: - ed.apply_to(fpath) - - -class WriteTransform(NamedTuple): - path: str - content: str - - @classmethod - def parse_data(self, data: Dict) -> 'WriteTransform': - return WriteTransform(data.pop('path'), data.pop('content')) - - def apply_to(self, p: Path) -> None: - fpath = p / self.path - print('Writing to file', p, self.content) - fpath.write_text(self.content) - - -class RemoveTransform(NamedTuple): - path: Path - only_matching: Sequence[str] = () - - @classmethod - def parse_data(self, d: Any) -> 'RemoveTransform': - p = d.pop('path') - pat = d.pop('only-matching') - return RemoveTransform(Path(p), pat) - - def apply_to(self, p: Path) -> None: - if p.is_dir(): - self._apply_dir(p) - else: - p.unlink() - - def _apply_dir(self, p: Path) -> None: - abspath = p / self.path - if not self.only_matching: - # Remove everything - if abspath.is_dir(): - better_rmtree(abspath) - else: - abspath.unlink() - return - - for pat in self.only_matching: - items = glob_if_exists(abspath, pat) - for f in items: - if f.is_dir(): - better_rmtree(f) - else: - f.unlink() - - -class FSTransform(NamedTuple): - copy: Optional[CopyTransform] = None - move: Optional[MoveTransform] = None - remove: Optional[RemoveTransform] = None - write: Optional[WriteTransform] = None - edit: Optional[EditTransform] = None - - def apply_to(self, p: Path) -> None: - for tr in (self.copy, self.move, self.remove, self.write, self.edit): - if tr: - tr.apply_to(p) - - @classmethod - def parse_data(self, data: Any) -> 'FSTransform': - move = data.pop('move', None) - copy = data.pop('copy', None) - remove = data.pop('remove', None) - write = data.pop('write', None) - edit = data.pop('edit', None) - return FSTransform( - copy=None if copy is None else CopyTransform.parse_data(copy), - move=None if move is None else MoveTransform.parse_data(move), - remove=None if remove is None else RemoveTransform.parse_data(remove), - write=None if write is None else WriteTransform.parse_data(write), - edit=None if edit is None else EditTransform.parse_data(edit), - ) - - -class HTTPRemoteSpec(NamedTuple): - url: str - transform: Sequence[FSTransform] - - @classmethod - def parse_data(cls, data: Dict[str, Any]) -> 'HTTPRemoteSpec': - url = data.pop('url') - trs = [FSTransform.parse_data(tr) for tr in data.pop('transforms', [])] - return HTTPRemoteSpec(url, trs) - - def make_local_dir(self): - return http_dl_unpack(self.url) - - -class GitSpec(NamedTuple): - url: str - ref: str - transform: Sequence[FSTransform] - - @classmethod - def parse_data(cls, data: Dict[str, Any]) -> 'GitSpec': - ref = data.pop('ref') - url = data.pop('url') - trs = [FSTransform.parse_data(tr) for tr in data.pop('transform', [])] - return GitSpec(url=url, ref=ref, transform=trs) - - @contextmanager - def make_local_dir(self) -> Iterator[Path]: - tdir = Path(tempfile.mkdtemp()) - try: - check_call(['git', 'clone', '--quiet', self.url, f'--depth=1', f'--branch={self.ref}', str(tdir)]) - yield tdir - finally: - better_rmtree(tdir) - - -class ForeignPackage(NamedTuple): - remote: Union[HTTPRemoteSpec, GitSpec] - transform: Sequence[FSTransform] - auto_lib: Optional[Tuple] - - @classmethod - def parse_data(cls, data: Dict[str, Any]) -> 'ForeignPackage': - git = data.pop('git', None) - http = data.pop('http', None) - chosen = git or http - assert chosen, data - trs = data.pop('transform', []) - al = data.pop('auto-lib', None) - return ForeignPackage( - remote=GitSpec.parse_data(git) if git else HTTPRemoteSpec.parse_data(http), - transform=[FSTransform.parse_data(tr) for tr in trs], - auto_lib=al.split('/') if al else None, - ) - - @contextmanager - def make_local_dir(self, name: str, ver: VersionInfo) -> Iterator[Path]: - with self.remote.make_local_dir() as tdir: - for tr in self.transform: - tr.apply_to(tdir) - if self.auto_lib: - pkg_json = { - 'name': name, - 'version': str(ver), - 'namespace': self.auto_lib[0], - } - lib_json = {'name': self.auto_lib[1]} - tdir.joinpath('package.jsonc').write_text(json.dumps(pkg_json)) - tdir.joinpath('library.jsonc').write_text(json.dumps(lib_json)) - yield tdir - - -class SpecPackage(NamedTuple): - name: str - version: VersionInfo - depends: Sequence[Dependency] - description: str - remote: ForeignPackage - - @classmethod - def parse_data(cls, name: str, version: str, data: Any) -> 'SpecPackage': - deps = data.pop('depends', []) - desc = data.pop('description', '[No description]') - remote = ForeignPackage.parse_data(data.pop('remote')) - return SpecPackage(name, - VersionInfo.parse(version), - description=desc, - depends=[Dependency.parse(d) for d in deps], - remote=remote) - - -def iter_spec(path: Path) -> Iterable[SpecPackage]: - data = json.loads(path.read_text()) - pkgs = data['packages'] - return iter_spec_packages(pkgs) - - -def iter_spec_packages(data: Dict[str, Any]) -> Iterable[SpecPackage]: - for name, versions in data.items(): - for version, defin in versions.items(): - yield SpecPackage.parse_data(name, version, defin) - - -def _on_rm_error_win32(fn, filepath, _exc_info): - p = Path(filepath) - p.chmod(stat.S_IWRITE) - p.unlink() - - -def better_rmtree(dir: Path) -> None: - if os.name == 'nt': - shutil.rmtree(dir, onerror=_on_rm_error_win32) - else: - shutil.rmtree(dir) - - -@contextmanager -def http_dl_unpack(url: str) -> Iterator[Path]: - req = request.urlopen(url) - tdir = Path(tempfile.mkdtemp()) - ofile = tdir / '.dl-archive' - try: - with ofile.open('wb') as fd: - fd.write(req.read()) - tf = tarfile.open(ofile) - tf.extractall(tdir) - tf.close() - ofile.unlink() - subdir = next(iter(Path(tdir).iterdir())) - yield subdir - finally: - better_rmtree(tdir) - - -@contextmanager -def spec_as_local_tgz(dds_exe: Path, spec: SpecPackage) -> Iterator[Path]: - with spec.remote.make_local_dir(spec.name, spec.version) as clone_dir: - out_tgz = clone_dir / 'sdist.tgz' - check_call([str(dds_exe), 'pkg', 'create', f'--project={clone_dir}', f'--out={out_tgz}']) - yield out_tgz - - -class Repository: - def __init__(self, dds_exe: Path, path: Path) -> None: - self._path = path - self._dds_exe = dds_exe - self._import_lock = Lock() - - @property - def pkg_dir(self) -> Path: - return self._path / 'pkg' - - @classmethod - def create(cls, dds_exe: Path, dirpath: Path, name: str) -> 'Repository': - check_call([str(dds_exe), 'repoman', 'init', str(dirpath), f'--name={name}']) - return Repository(dds_exe, dirpath) - - @classmethod - def open(cls, dds_exe: Path, dirpath: Path) -> 'Repository': - return Repository(dds_exe, dirpath) - - def import_tgz(self, path: Path) -> None: - check_call([str(self._dds_exe), 'repoman', 'import', str(self._path), str(path)]) - - def remove(self, name: str) -> None: - check_call([str(self._dds_exe), 'repoman', 'remove', str(self._path), name]) - - def spec_import(self, spec: Path) -> None: - all_specs = iter_spec(spec) - want_import = (s for s in all_specs if self._shoule_import(s)) - pool = ThreadPoolExecutor(10) - futs = pool.map(self._get_and_import, want_import) - for res in futs: - pass - - def _shoule_import(self, spec: SpecPackage) -> bool: - expect_file = self.pkg_dir / spec.name / str(spec.version) / 'sdist.tar.gz' - return not expect_file.is_file() - - def _get_and_import(self, spec: SpecPackage) -> None: - print(f'Import: {spec.name}@{spec.version}') - with spec_as_local_tgz(self._dds_exe, spec) as tgz: - with self._import_lock: - self.import_tgz(tgz) - - -class Arguments(Protocol): - dir: Path - spec: Path - dds_exe: Path - - -def main(argv: Sequence[str]) -> int: - parser = argparse.ArgumentParser() - parser.add_argument('--dds-exe', type=Path, help='Path to the dds executable to use', default=_get_dds_exe()) - parser.add_argument('--dir', '-d', help='Path to a repository to manage', required=True, type=Path) - parser.add_argument('--spec', - metavar='', - type=Path, - required=True, - help='Provide a JSON document specifying how to obtain an import some packages') - args: Arguments = parser.parse_args(argv) - repo = Repository.open(args.dds_exe, args.dir) - repo.spec_import(args.spec) - - return 0 - - -def start() -> NoReturn: - sys.exit(main(sys.argv[1:])) - - -if __name__ == "__main__": - start() diff --git a/tools/prep-catch2.py b/tools/prep-catch2.py deleted file mode 100644 index 9c38a75f..00000000 --- a/tools/prep-catch2.py +++ /dev/null @@ -1,61 +0,0 @@ -from pathlib import Path -import gzip - -ROOT = Path(__file__).absolute().parent.parent -c2_header = ROOT / 'res/catch2.hpp' - -buf = c2_header.read_bytes() -compr = gzip.compress(buf, compresslevel=9) -chars = ', '.join(f"'\\x{b:02x}'" for b in compr) - - -def oct_encode_one(b: int) -> str: - if b >= 33 and b <= 126: - c = chr(b) - if c in ('"', '\\'): - return '\\' + c - return c - else: - return f'\\{oct(b)[2:]:>03}' - - -def oct_encode(b: bytes) -> str: - return ''.join(oct_encode_one(byt) for byt in b) - - -bufs = [] -while compr: - head = compr[:2000] - compr = compr[len(head):] - octl = oct_encode(head) - bufs.append(f'"{octl}"_buf') - -bufs_arr = ',\n '.join(bufs) - -c2_embedded = ROOT / 'src/dds/catch2_embedded.generated.cpp' -c2_embedded.write_text(f''' -#include "./catch2_embedded.hpp" - -#include -#include - -using namespace neo::literals; - -namespace dds::detail {{ - -static const neo::const_buffer catch2_gzip_bufs[] = {{ - {bufs_arr} -}}; - -}} - -std::string_view dds::detail::catch2_embedded_single_header_str() noexcept {{ - static const std::string decompressed = [] {{ - neo::string_dynbuf_io str; - neo::gzip_decompress(str, catch2_gzip_bufs); - str.shrink_uncommitted(); - return std::move(str.string()); - }}(); - return decompressed; -}} -''') diff --git a/tools/windows-openssl.tgz b/tools/windows-openssl.tgz new file mode 100644 index 00000000..53877a9d Binary files /dev/null and b/tools/windows-openssl.tgz differ