diff options
author | Marcus Tillmanns <[email protected]> | 2025-06-26 12:39:06 +0200 |
---|---|---|
committer | Marcus Tillmanns <[email protected]> | 2025-07-01 13:37:06 +0000 |
commit | a8d832c3b83105f934197ebf7a2f2e42e6c15990 (patch) | |
tree | 550ac3140dee815aa93c2a787c3d48159debb728 | |
parent | bea33f47bfdf002eb5095c0de06107d34db32e8a (diff) |
Change-Id: Ia7d38d5234ab12b32b259a09abd797d22d197298
Reviewed-by: hjk <[email protected]>
-rw-r--r-- | src/libs/devcontainer/devcontainer.cpp | 173 | ||||
-rw-r--r-- | src/libs/devcontainer/devcontainer.h | 13 | ||||
-rw-r--r-- | tests/auto/devcontainer/tst_devcontainer.cpp | 32 |
3 files changed, 137 insertions, 81 deletions
diff --git a/src/libs/devcontainer/devcontainer.cpp b/src/libs/devcontainer/devcontainer.cpp index 0d2bf573752..f60c4461ffa 100644 --- a/src/libs/devcontainer/devcontainer.cpp +++ b/src/libs/devcontainer/devcontainer.cpp @@ -41,62 +41,67 @@ QString InstanceConfig::devContainerId() const return id; } -QString InstanceConfig::jsonToString(const QJsonValue &value) const +using Replacers = QMap<QString, std::function<QString(QStringList)>>; + +static void substituteVariables(QString &str, const Replacers &replacers) { - QString str = value.toString(); QRegularExpression re("\\$\\{([^}]+)\\}"); QRegularExpressionMatchIterator it = re.globalMatch(str); - if (it.hasNext()) { - const QMap<QString, std::function<QString(QStringList)>> replacers = { - {"localWorkspaceFolder", [this](const QStringList &) { return workspaceFolder.path(); }}, - {"localWorkspaceFolderBasename", - [this](const QStringList &) { return workspaceFolder.fileName(); }}, - {"containerWorkspaceFolder", - [this](const QStringList &) { return containerWorkspaceFolder.path(); }}, - {"containerWorkspaceFolderBasename", - [this](const QStringList &) { return containerWorkspaceFolder.fileName(); }}, - {"devcontainerId", [this](const QStringList &) { return devContainerId(); }}, - {"localEnv", - [this](const QStringList &parts) { - if (parts.isEmpty()) - return QString(); - const QString varname = parts.first(); - const QString defaultValue = parts.mid(1).join(':'); - return localEnvironment.value_or(varname, defaultValue); - }}, - }; - - struct Replace - { - qsizetype start; - qsizetype length; - QString replacement; - }; - QList<Replace> replacements; - - while (it.hasNext()) { - QRegularExpressionMatch match = it.next(); - QString varName = match.captured(1); - QStringList parts = varName.split(':'); - QString variableName = parts.takeFirst(); - - auto itReplacer = replacers.find(variableName); - if (itReplacer != replacers.end()) { - QString replacement = itReplacer.value()(parts); - replacements.append({match.capturedStart(), match.capturedLength(), replacement}); - } else if (variableName != "containerEnv") { - // Container env is only supported for "remoteEnv", but since it might be valid, - // we don't warn about it here. - logFunction(Tr::tr("Unsupported variable in devcontainer config:") + variableName); - } + struct Replace + { + qsizetype start; + qsizetype length; + QString replacement; + }; + QList<Replace> replacements; + + while (it.hasNext()) { + QRegularExpressionMatch match = it.next(); + QString varName = match.captured(1); + QStringList parts = varName.split(':'); + QString variableName = parts.takeFirst(); + + auto itReplacer = replacers.find(variableName); + if (itReplacer != replacers.end()) { + QString replacement = itReplacer.value()(parts); + replacements.append({match.capturedStart(), match.capturedLength(), replacement}); + } else { + qCWarning(devcontainerlog) + << Tr::tr("Unsupported variable in devcontainer config:") << variableName; } - - // Apply replacements in reverse order to avoid messing up indices - for (auto it = replacements.crbegin(); it != replacements.crend(); ++it) - str.replace(it->start, it->length, std::move(it->replacement)); } + // Apply replacements in reverse order to avoid messing up indices + for (auto it = replacements.crbegin(); it != replacements.crend(); ++it) + str.replace(it->start, it->length, std::move(it->replacement)); +} + +QString InstanceConfig::jsonToString(const QJsonValue &value) const +{ + QString str = value.toString(); + + const Replacers replacers + = {{"localWorkspaceFolder", [this](const QStringList &) { return workspaceFolder.path(); }}, + {"localWorkspaceFolderBasename", + [this](const QStringList &) { return workspaceFolder.fileName(); }}, + {"containerWorkspaceFolder", + [this](const QStringList &) { return containerWorkspaceFolder.path(); }}, + {"containerWorkspaceFolderBasename", + [this](const QStringList &) { return containerWorkspaceFolder.fileName(); }}, + {"devcontainerId", [this](const QStringList &) { return devContainerId(); }}, + {"localEnv", + [this](const QStringList &parts) { + if (parts.isEmpty()) + return QString(); + const QString varname = parts.first(); + const QString defaultValue = parts.mid(1).join(':'); + return localEnvironment.value_or(varname, defaultValue); + }}, + {"containerEnv", + [](const QStringList &parts) { return QString("${%1}").arg(parts.join(':')); }}}; + + substituteVariables(str, replacers); return str; } @@ -1213,7 +1218,8 @@ static ExecutableItem startContainerRecipe(const InstanceConfig &instanceConfig) static Result<Group> prepareContainerRecipe( const DockerfileContainer &containerConfig, const DevContainerCommon &commonConfig, - const InstanceConfig &instanceConfig) + const InstanceConfig &instanceConfig, + const RunningInstance &runningInstance) { const auto setupBuildImage = [containerConfig, instanceConfig](Process &process) { connectProcessToLog(process, instanceConfig, Tr::tr("Build Dockerfile")); @@ -1244,7 +1250,6 @@ static Result<Group> prepareContainerRecipe( imageDetails, runningDetails, containerDetails, - ProcessTask(setupBuildImage), inspectImageTask(imageDetails, instanceConfig, imageName(instanceConfig)), createContainerRecipe( @@ -1253,6 +1258,9 @@ static Result<Group> prepareContainerRecipe( startContainerRecipe(instanceConfig), runningContainerDetailsTask(containerDetails, runningDetails, commonConfig, instanceConfig), runLifecycleHooksRecipe(commonConfig, instanceConfig), + Sync([runningInstance, runningDetails](){ + runningInstance->remoteEnvironment = runningDetails->probedUserEnvironment; + }), }; // clang-format on } @@ -1298,7 +1306,8 @@ static ExecutableItem prepareDockerImageRecipe( static Result<Group> prepareContainerRecipe( const ImageContainer &imageConfig, const DevContainerCommon &commonConfig, - const InstanceConfig &instanceConfig) + const InstanceConfig &instanceConfig, + const RunningInstance &runningInstance) { Storage<ImageDetails> imageDetails; Storage<ContainerDetails> containerDetails; @@ -1315,6 +1324,9 @@ static Result<Group> prepareContainerRecipe( startContainerRecipe(instanceConfig), runningContainerDetailsTask(containerDetails, runningDetails, commonConfig, instanceConfig), runLifecycleHooksRecipe(commonConfig, instanceConfig), + Sync([runningInstance, runningDetails](){ + runningInstance->remoteEnvironment = runningDetails->probedUserEnvironment; + }), }; // clang-format on } @@ -1322,9 +1334,11 @@ static Result<Group> prepareContainerRecipe( static Result<Group> prepareContainerRecipe( const ComposeContainer &config, const DevContainerCommon &commonConfig, - const InstanceConfig &instanceConfig) + const InstanceConfig &instanceConfig, + const RunningInstance &runningInstance) { Q_UNUSED(commonConfig); + Q_UNUSED(runningInstance); const auto setupComposeUp = [config, instanceConfig](Process &process) { connectProcessToLog(process, instanceConfig, "Compose Up"); @@ -1370,11 +1384,16 @@ static Result<Group> prepareContainerRecipe( return ResultError("Docker Compose is not yet supported in DevContainer."); } -static Result<Group> prepareRecipe(const Config &config, const InstanceConfig &instanceConfig) +static Result<Group> prepareRecipe( + const Config &config, + const InstanceConfig &instanceConfig, + const RunningInstance &runningInstance) { return std::visit( - [&instanceConfig, commonConfig = config.common](const auto &containerConfig) { - return prepareContainerRecipe(containerConfig, commonConfig, instanceConfig); + [&instanceConfig, commonConfig = config.common, runningInstance]( + const auto &containerConfig) { + return prepareContainerRecipe( + containerConfig, commonConfig, instanceConfig, runningInstance); }, *config.containerConfig); } @@ -1449,12 +1468,12 @@ static Result<Group> downRecipe(const Config &config, const InstanceConfig &inst *config.containerConfig); } -Result<> Instance::up() +Result<> Instance::up(const RunningInstance &runningInstance) { if (!d->config.containerConfig) return ResultOk; - const Utils::Result<Tasking::Group> recipeResult = upRecipe(); + const Utils::Result<Tasking::Group> recipeResult = upRecipe(runningInstance); if (!recipeResult) return ResultError(recipeResult.error()); @@ -1478,9 +1497,12 @@ Result<> Instance::down() return ResultOk; } -Result<Tasking::Group> Instance::upRecipe() const +Result<Tasking::Group> Instance::upRecipe(const RunningInstance &runningInstance) const { - return prepareRecipe(d->config, d->instanceConfig); + if (!runningInstance) + return ResultError(Tr::tr("Running instance cannot be null.")); + + return prepareRecipe(d->config, d->instanceConfig, runningInstance); } Result<Tasking::Group> Instance::downRecipe() const @@ -1496,9 +1518,13 @@ const Config &Instance::config() const class DevContainerProcessInterface : public Utils::WrappedProcessInterface { public: - DevContainerProcessInterface(const Config &config, const InstanceConfig &instanceConfig) + DevContainerProcessInterface( + const Config &config, + const InstanceConfig &instanceConfig, + const RunningInstance &runningInstance) : m_config(config) , m_instanceConfig(instanceConfig) + , m_runningInstance(runningInstance) {} Result<CommandLine> wrapCommmandLine( @@ -1521,10 +1547,21 @@ public: QStringList unsetKeys; Environment remoteEnv; for (const auto &[k, v] : m_config.common.remoteEnv) { - if (v) - remoteEnv.set(k, *v); - else - remoteEnv.set(k, {}, false); // We use the disabled state to unset the variable. + if (v) { + QString value = *v; + const Replacers replacers = { + {"containerEnv", [this](const QStringList &parts) { + if (parts.isEmpty()) + return QString(); + const QString varname = parts.first(); + const QString defaultValue = parts.mid(1).join(':'); + return m_runningInstance->remoteEnvironment.value_or(varname, defaultValue); + }}}; + substituteVariables(value, replacers); + remoteEnv.set(k, value); + } else { + remoteEnv.set(k, {}, false); // We use the disabled state to unset the variable.} + } } const Environment env = setupData.m_environment.appliedToEnvironment(remoteEnv); @@ -1589,11 +1626,13 @@ public: protected: Config m_config; InstanceConfig m_instanceConfig; + RunningInstance m_runningInstance; }; -ProcessInterface *Instance::createProcessInterface() const +ProcessInterface *Instance::createProcessInterface(const RunningInstance &runningInstance) const { - return new DevContainerProcessInterface(d->config, d->instanceConfig); + QTC_ASSERT(runningInstance, return nullptr); + return new DevContainerProcessInterface(d->config, d->instanceConfig, runningInstance); } } // namespace DevContainer diff --git a/src/libs/devcontainer/devcontainer.h b/src/libs/devcontainer/devcontainer.h index 93683b2699c..40fcffe2600 100644 --- a/src/libs/devcontainer/devcontainer.h +++ b/src/libs/devcontainer/devcontainer.h @@ -38,6 +38,13 @@ struct DEVCONTAINER_EXPORT InstanceConfig QString devContainerId() const; }; +struct DEVCONTAINER_EXPORT RunningInstanceData +{ + Utils::Environment remoteEnvironment; +}; + +using RunningInstance = std::shared_ptr<RunningInstanceData>; + class DEVCONTAINER_EXPORT Instance { public: @@ -52,12 +59,12 @@ public: ~Instance(); - Utils::Result<> up(); // Create and start the container + Utils::Result<> up(const RunningInstance &runningInstance); // Create and start the container Utils::Result<> down(); // Stop and remove the container - Utils::ProcessInterface *createProcessInterface() const; + Utils::ProcessInterface *createProcessInterface(const RunningInstance &runningInstance) const; - Utils::Result<Tasking::Group> upRecipe() const; + Utils::Result<Tasking::Group> upRecipe(const RunningInstance &runningInstance) const; Utils::Result<Tasking::Group> downRecipe() const; const Config &config() const; diff --git a/tests/auto/devcontainer/tst_devcontainer.cpp b/tests/auto/devcontainer/tst_devcontainer.cpp index bd3362e7be7..9894132d756 100644 --- a/tests/auto/devcontainer/tst_devcontainer.cpp +++ b/tests/auto/devcontainer/tst_devcontainer.cpp @@ -276,7 +276,9 @@ FROM alpine:latest std::unique_ptr<DevContainer::Instance> instance = DevContainer::Instance::fromConfig(config, instanceConfig); - Utils::Result<Tasking::Group> recipe = instance->upRecipe(); + DevContainer::RunningInstance runningInstance + = std::make_shared<DevContainer::RunningInstanceData>(); + Utils::Result<Tasking::Group> recipe = instance->upRecipe(runningInstance); QVERIFY_RESULT(recipe); QCOMPARE( Tasking::TaskTree::runBlocking((*recipe).withTimeout(recipeTimeout)), @@ -308,7 +310,9 @@ void tst_DevContainer::upImage() std::unique_ptr<DevContainer::Instance> instance = DevContainer::Instance::fromConfig(config, instanceConfig); - Utils::Result<Tasking::Group> recipe = instance->upRecipe(); + DevContainer::RunningInstance runningInstance + = std::make_shared<DevContainer::RunningInstanceData>(); + Utils::Result<Tasking::Group> recipe = instance->upRecipe(runningInstance); QVERIFY_RESULT(recipe); QCOMPARE( Tasking::TaskTree::runBlocking((*recipe).withTimeout(recipeTimeout)), @@ -353,7 +357,9 @@ void tst_DevContainer::upWithHooks() std::unique_ptr<DevContainer::Instance> instance = DevContainer::Instance::fromConfig(config, instanceConfig); - Utils::Result<Tasking::Group> recipe = instance->upRecipe(); + DevContainer::RunningInstance runningInstance + = std::make_shared<DevContainer::RunningInstanceData>(); + Utils::Result<Tasking::Group> recipe = instance->upRecipe(runningInstance); QVERIFY_RESULT(recipe); QCOMPARE( Tasking::TaskTree::runBlocking((*recipe).withTimeout(recipeTimeout)), @@ -381,12 +387,12 @@ void tst_DevContainer::processInterface() {"CONTAINER_CHANGE_ME", "container_value_to_change"}, }; - config.common.remoteEnv = { - {"TEST_VAR", "test_value"}, - {"ANOTHER_VAR", "another_value"}, - {"CONTAINER_UNSET_ME", std::nullopt}, - {"CONTAINER_CHANGE_ME", "changed_container_value"}, - }; + config.common.remoteEnv + = {{"TEST_VAR", "test_value"}, + {"ANOTHER_VAR", "another_value"}, + {"CONTAINER_UNSET_ME", std::nullopt}, + {"CONTAINER_CHANGE_ME", "changed_container_value"}, + {"REMOTEENV_FROM_CONTAINER", "${containerEnv:CONTAINER_TEST}"}}; qDebug() << "DevContainer Config:" << config; DevContainer::InstanceConfig instanceConfig{ @@ -399,7 +405,9 @@ void tst_DevContainer::processInterface() std::unique_ptr<DevContainer::Instance> instance = DevContainer::Instance::fromConfig(config, instanceConfig); - Utils::Result<Tasking::Group> recipe = instance->upRecipe(); + DevContainer::RunningInstance runningInstance + = std::make_shared<DevContainer::RunningInstanceData>(); + Utils::Result<Tasking::Group> recipe = instance->upRecipe(runningInstance); QVERIFY_RESULT(recipe); QCOMPARE( Tasking::TaskTree::runBlocking((*recipe).withTimeout(recipeTimeout)), @@ -412,7 +420,8 @@ void tst_DevContainer::processInterface() testEnv.set("CONTAINER_TEST", "", false); process.setEnvironment(testEnv); - process.setProcessInterfaceCreator([&instance]() { return instance->createProcessInterface(); }); + process.setProcessInterfaceCreator( + [&]() { return instance->createProcessInterface(runningInstance); }); process.setCommand({"printenv", {}}); process.runBlocking(std::chrono::seconds(10), EventLoopMode::On); const QString output = process.cleanedStdOut().trimmed(); @@ -430,6 +439,7 @@ void tst_DevContainer::processInterface() QCOMPARE(firstEnv.value("TEST_VAR"), "test_value"); QCOMPARE(firstEnv.value("ANOTHER_VAR"), "another_value"); QCOMPARE(firstEnv.value("CONTAINER_CHANGE_ME"), "changed_container_value"); + QCOMPARE(firstEnv.value("REMOTEENV_FROM_CONTAINER"), "test_value_container"); Utils::Result<Tasking::Group> downRecipe = instance->downRecipe(); QVERIFY_RESULT(downRecipe); |