aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMarcus Tillmanns <[email protected]>2025-06-26 12:39:06 +0200
committerMarcus Tillmanns <[email protected]>2025-07-01 13:37:06 +0000
commita8d832c3b83105f934197ebf7a2f2e42e6c15990 (patch)
tree550ac3140dee815aa93c2a787c3d48159debb728
parentbea33f47bfdf002eb5095c0de06107d34db32e8a (diff)
Devcontainer: Add support for "containerEnv" variable substitutionHEADmaster
-rw-r--r--src/libs/devcontainer/devcontainer.cpp173
-rw-r--r--src/libs/devcontainer/devcontainer.h13
-rw-r--r--tests/auto/devcontainer/tst_devcontainer.cpp32
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);