Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support dub's actual (and special) dub test configuration #173

Merged
merged 14 commits into from
Nov 23, 2022
Merged
4 changes: 2 additions & 2 deletions .github/workflows/d.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
# Remember to change the compiler versions further below as well as here
dc:
- dmd-2.100.0
- ldc-1.29.0
- ldc-1.30.0

runs-on: ${{ matrix.os }}
steps:
Expand Down Expand Up @@ -65,7 +65,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-20.04, macos-10.15, windows-2019]
dc: [dmd-2.098.0, ldc-1.28.0]
dc: [dmd-2.100.0, ldc-1.30.0]
runs-on: ${{ matrix.os }}
defaults:
run:
Expand Down
3 changes: 1 addition & 2 deletions doc/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,7 @@ flags, an empty parameter list must be added, e.g.:
dubTestTarget
-------------

The target that would be built by `dub test`, with the difference that the executable
name is always `ut`.
The target that would be built by `dub test`.

dubConfigurationTarget
----------------------
Expand Down
3 changes: 1 addition & 2 deletions dub.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"excludedSourceFiles": ["payload/reggae/buildgen_main.d", "payload/reggae/dcompile.d"],
"mainSourceFile": "src/reggae/reggae_main.d",
"dependencies": {
"dub": "==1.29.0"
"dub": "==1.30.0"
},
"subConfigurations": {
"dub": "library"
Expand All @@ -27,7 +27,6 @@
"sourcePaths": ["tests", "payload"],
"mainSourceFile": "tests/ut_main.d",
"versions": ["ReggaeTest"],
"dflags": ["-allinst"],
"excludedSourceFiles": ["payload/reggae/buildgen_main.d",
"src/reggae/reggae_main.d",
"tests/projects/project1/src/main.d",
Expand Down
2 changes: 1 addition & 1 deletion dub.selections.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"fileVersion": 1,
"versions": {
"dub": "1.29.0",
"dub": "1.30.0",
"unit-threaded": "2.0.5"
}
}
43 changes: 22 additions & 21 deletions payload/reggae/rules/dub.d
Original file line number Diff line number Diff line change
Expand Up @@ -81,30 +81,31 @@ static if(isDubProject) {
{
import reggae.dub.info: TargetType, targetName;
import std.exception : enforce;
import std.conv: text;

const config = "unittest" in configToDubInfo ? "unittest" : "default";
auto actualCompilerFlags = compilerFlags.value;
if("unittest" !in configToDubInfo) actualCompilerFlags ~= "-unittest";
const dubInfo = configToDubInfo[config];
enforce(dubInfo.packages.length, text("No dub packages found for config '", config, "'"));
const hasMain = dubInfo.packages[0].mainSourceFile != "";
const string[] emptyStrings;
const extraLinkerFlags = hasMain ? emptyStrings : ["-main"];
const actualLinkerFlags = extraLinkerFlags ~ linkerFlags.value;
const defaultTargetHasName = configToDubInfo["default"].packages.length > 0;
const sameNameAsDefaultTarget =
defaultTargetHasName
&& dubInfo.targetName == configToDubInfo["default"].targetName;
const name = sameNameAsDefaultTarget
// don't emit two targets with the same name
? targetName(TargetType.executable, "ut")
: dubInfo.targetName;

// No `dub test` config? Then it inherited some `targetType "none"`, and
// dub has printed an according message - return a dummy target and continue.
// [Similarly, `dub test` is a no-op and returns success in such scenarios.]
if ("unittest" !in configToDubInfo)
return Target(null);

const dubInfo = configToDubInfo["unittest"];
enforce(dubInfo.packages.length, "No dub packages found for the dub test configuration");
enforce(dubInfo.packages[0].mainSourceFile.length, "No mainSourceFile for the dub test configuration");

auto name = dubInfo.targetName;
const defaultDubInfo = configToDubInfo["default"];
if (defaultDubInfo.packages.length > 0 && defaultDubInfo.targetName == name) {
// The targetName of both default & test configs conflict (due to a bad
// `unittest` config in dub.{sdl,json}).
// Rename the test target to `ut[.exe]` to prevent conflicts in Ninja/make
// build scripts (in case the default config is included in the build too).
name = targetName(TargetType.executable, "ut");
}

return dubTarget(name,
dubInfo,
actualCompilerFlags,
actualLinkerFlags,
compilerFlags.value,
linkerFlags.value,
compilationMode);
}

Expand Down
1 change: 1 addition & 0 deletions src/reggae/dub/interop/configurations.d
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ import reggae.from;
struct DubConfigurations {
string[] configurations;
string default_;
string test; // special `dub test` config
}
27 changes: 22 additions & 5 deletions src/reggae/dub/interop/dublib.d
Original file line number Diff line number Diff line change
Expand Up @@ -74,24 +74,41 @@ struct Dub {
return ret;
}

DubConfigurations getConfigs(/*in*/ ref from!"dub.platform".BuildPlatform platform) {

DubConfigurations getConfigs
(in from!"dub.generators.generator".GeneratorSettings settings, in string singleConfig = null)
{
import std.algorithm.iteration: filter, map;
import std.array: array;

const allConfigs = singleConfig == "";

// add the special `dub test` configuration (which doesn't require an existing `unittest` config)
const testConfig = (allConfigs || singleConfig == "unittest")
? _project.addTestRunnerConfiguration(settings)
: null; // skip when requesting a single non-unittest config
const haveSpecialTestConfig = testConfig.length && testConfig != "unittest";

if (!allConfigs) {
// translate `unittest` to the actual test configuration
const config = haveSpecialTestConfig ? testConfig : singleConfig;
return DubConfigurations([config], config, testConfig);
}

// A violation of the Law of Demeter caused by a dub bug.
// Otherwise _project.configurations would do, but it fails for one
// projet and no reduced test case was found.
auto configurations = _project
.rootPackage
.recipe
.configurations
.filter!(c => c.matchesPlatform(platform))
.filter!(c => c.matchesPlatform(settings.platform))
.map!(c => c.name)
// exclude unittest config if there's a derived special one
.filter!(n => !haveSpecialTestConfig || n != "unittest")
.array;

// Project.getDefaultConfiguration() requires a mutable arg (forgotten `in`)
return DubConfigurations(configurations, _project.getDefaultConfiguration(platform));
const defaultConfig = _project.getDefaultConfiguration(settings.platform);
return DubConfigurations(configurations, defaultConfig, testConfig);
}

DubInfo configToDubInfo
Expand Down
87 changes: 51 additions & 36 deletions src/reggae/dub/interop/package.d
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public import reggae.dub.interop.reggaefile;
from!"reggae.dub.info".DubInfo[string] gDubInfos;


void writeDubConfig(T)(auto ref T output,
void writeDubConfig(O)(ref O output,
in from!"reggae.options".Options options,
from!"std.stdio".File file) {
import reggae.io: log;
Expand Down Expand Up @@ -83,8 +83,8 @@ private string ensureDubSelectionsJson


private from!"reggae.dub.info".DubInfo getDubInfo
(T)
(auto ref T output,
(O)
(ref O output,
ref from!"reggae.dub.interop.dublib".Dub dub,
in from!"reggae.options".Options options)
{
Expand All @@ -103,26 +103,33 @@ private from!"reggae.dub.info".DubInfo getDubInfo

auto settings = dub.getGeneratorSettings(options);
const configs = dubConfigurations(output, dub, options, settings);
const haveTestConfig = configs.test != "";
bool atLeastOneConfigOk;
Exception dubInfoFailure;

foreach(config; configs.configurations) {
const isTestConfig = haveTestConfig && config == configs.test;
try {
handleDubConfig(output, dub, options, settings, config);
gDubInfos[config] = handleDubConfig(output, dub, options, settings, config, isTestConfig);
atLeastOneConfigOk = true;
} catch(Exception ex) {
output.log("ERROR: Could not get info for configuration ", config, ": ", ex.msg);
if(dubInfoFailure is null) dubInfoFailure = ex;
}
}

gDubInfos["default"] = gDubInfos[configs.default_];

if(!atLeastOneConfigOk) {
assert(dubInfoFailure !is null,
"Internal error: no configurations worked and no exception to throw");
throw dubInfoFailure;
}

gDubInfos["default"] = gDubInfos[configs.default_];

// (additionally) expose the special `dub test` config as `unittest` config in the DSL (`configToDubInfo`)
// (for `dubTestTarget!()`, `dubConfigurationTarget!(Configuration("unittest"))` etc.)
if(haveTestConfig && configs.test != "unittest" && configs.test in gDubInfos)
gDubInfos["unittest"] = gDubInfos[configs.test];
}

return gDubInfos["default"];
Expand All @@ -134,68 +141,76 @@ dubConfigurations
(ref O output,
ref from!"reggae.dub.interop.dublib".Dub dub,
in from!"reggae.options".Options options,
from!"dub.generators.generator".GeneratorSettings settings)
in from!"dub.generators.generator".GeneratorSettings settings)
{
import reggae.dub.interop.configurations: DubConfigurations;
import reggae.io: log;

if(options.dubConfig == "") {
const allConfigs = options.dubConfig == "";

output.log("Getting dub configurations");
auto ret = dub.getConfigs(settings.platform);
output.log("Number of dub configurations: ", ret.configurations.length);
if(allConfigs) output.log("Getting dub configurations");
auto ret = dub.getConfigs(settings, options.dubConfig);
if(allConfigs) output.log("Number of dub configurations: ", ret.configurations.length);

// this happens e.g. the targetType is "none"
if(ret.configurations.length == 0)
return DubConfigurations([""], "");

return ret;
} else {
return DubConfigurations([options.dubConfig], options.dubConfig);
// error out if the test config is explicitly requested but not available
if(options.dubConfig == "unittest" && ret.test == "") {
output.log("ERROR: No dub test configuration available (target type 'none'?)");
throw new Exception("No dub test configuration");
}

// this happens e.g. the targetType is "none"
if(ret.configurations.length == 0)
return DubConfigurations([""], "", null);

return ret;
}

private void handleDubConfig
private from!"reggae.dub.info".DubInfo handleDubConfig
(O)
(ref O output,
ref from!"reggae.dub.interop.dublib".Dub dub,
in from!"reggae.options".Options options,
from!"dub.generators.generator".GeneratorSettings settings,
in string config)
in string config,
in bool isTestConfig)
{
import reggae.io: log;
import std.conv: text;

output.log("Querying dub configuration '", config, "'");
gDubInfos[config] = dub.configToDubInfo(settings, config);

// dub adds certain flags to certain configurations automatically but these flags
// don't know up in the output to `dub describe`. Special case them here.

// unittest should only apply to the main package, hence [0].
// This doesn't show up in `dub describe`, it's secret info that dub knows
// so we have to add it manually here.
if(config == "unittest") {
if(config !in gDubInfos)
throw new Exception(
text("Configuration `", config, "` not found in ",
() @trusted { return gDubInfos.keys; }()));
if(gDubInfos[config].packages.length == 0)
auto dubInfo = dub.configToDubInfo(settings, config);

/**
For the `dub test` config, add `-unittest` (only for the main package, hence [0]).
[Similarly, `dub test` implies `--build=unittest`, with the unittest build type
being the debug one + `-unittest`.]

This enables (assuming no custom reggaefile.d):
* `reggae && ninja default ut`
=> default `debug` build type for default config, extra `-unittest` for test config
* `reggae --dub-config=unittest && ninja`
=> no need for extra `--dub-build-type=unittest`
*/
if(isTestConfig) {
if(dubInfo.packages.length == 0)
throw new Exception(
text("No main package in `", config, "` configuration"));
gDubInfos[config].packages[0].dflags ~= "-unittest";
dubInfo.packages[0].dflags ~= "-unittest";
}

try
callPreBuildCommands(output, options, gDubInfos[config]);
callPreBuildCommands(output, options, dubInfo);
catch(Exception e) {
output.log("Error calling prebuild commands: ", e.msg);
throw e;
}

return dubInfo;
}


private void callPreBuildCommands(T)(auto ref T output,
private void callPreBuildCommands(O)(ref O output,
in from!"reggae.options".Options options,
in from!"reggae.dub.info".DubInfo dubInfo)
@safe
Expand Down
26 changes: 16 additions & 10 deletions src/reggae/dub/interop/reggaefile.d
Original file line number Diff line number Diff line change
Expand Up @@ -46,26 +46,32 @@ private enum standardDubReggaefile = q{
import reggae;

alias buildTarget = dubDefaultTarget!(); // dub build
alias testTarget = dubTestTarget!(); // dub test (=> ut[.exe])
alias testTarget = dubTestTarget!(); // dub test

Target aliasTarget(string aliasName, alias target)() {
import std.algorithm: map;
import std.algorithm: canFind, map;
const rawOutputs = target.rawOutputs;

// If the aliased target has an output with the same name, return a dummy target which
// won't make it to the Ninja/make build scripts.
// E.g., no conflicting `ut` alias target if the test target already produces a `ut` executable.
version (Windows) { /* all outputs feature some file extension */ }
else if (rawOutputs.canFind(aliasName) || rawOutputs.canFind("./" ~ aliasName))
return Target(null);

// Using a leaf target with `$builddir/<raw output>` outputs as dependency
// yields the expected relative target names for Ninja/make.
return Target.phony(aliasName, "", Target(target.rawOutputs.map!(o => "$builddir/" ~ o), ""));
return Target.phony(aliasName, "", Target(rawOutputs.map!(o => "$builddir/" ~ o), ""));
}

// Add a `default` convenience alias for the `dub build` target.
// Especially useful for Ninja (`ninja default ut` to build default & test targets in parallel).
alias defaultTarget = aliasTarget!("default", buildTarget);

version (Windows) {
// Windows: extra `ut` convenience alias for `ut.exe`
alias utTarget = aliasTarget!("ut", testTarget);
mixin build!(buildTarget, optional!testTarget, optional!defaultTarget, optional!utTarget);
} else {
mixin build!(buildTarget, optional!testTarget, optional!defaultTarget);
}
// And a `ut` convenience alias for the `dub test` target.
alias utTarget = aliasTarget!("ut", testTarget);

mixin build!(buildTarget, optional!testTarget, optional!defaultTarget, optional!utTarget);
};


Expand Down
Loading