setupStepsOf function
The steps by hand of the apps of modules, the modules of smf create,
each once by its id, for the catalog (see SetupCatalog.next).
The contract harness validates, without rendering, the apps of every
case of ContractHarness.checkAll and the apps with every module
(ContractHarness.casesOfAll), and a step is in the catalog when one of
those apps has it (see ContractResult.manualSteps). The apps of the
other values of the mode options of the roles have the steps of the
apps with the first values, since no step depends on what a role chose
(see RoleTemplate.manualSteps).
Of a step, SetupStep.neededBy has each module that lists it, in an app, itself or in a variant, and for the template of a role that lists it, the modules that provide the role in that app. The steps come in the order that an app with every module lists them in, those of the first such app first, which has the first provider of each role, then those that only the next apps with every module have, in their order, and then the steps of the smaller apps by id: those of a hook that lists a step for an app without some role, for instance. So the order of the steps that a developer of a full app does stays, and the file changes only with the steps of the modules.
Throws a StateError when an app has errors, since the steps of such
an app would come from modules that smf create refuses, and when two
apps give one step two titles, which the pipeline cannot see: it checks
the titles of a step within an app.
Implementation
Future<List<SetupStep>> setupStepsOf(List<SmfModule> modules) async {
final harness = ContractHarness(ModuleRegistry(modules), render: false);
final everyModule = [
for (final contractCase in harness.casesOfAll())
await harness.check(contractCase),
];
final results = [...everyModule, ...await harness.checkAll()];
final failed = [
for (final result in results)
if (result.errors.isNotEmpty)
'${result.contractCase.name}: ${result.errors.join(' ')}',
];
if (failed.isNotEmpty) {
throw StateError(
'The apps of the steps by hand have errors:\n${failed.join('\n')}',
);
}
final steps = <String, (ManualStep, String)>{};
final neededBy = <String, Set<String>>{};
final ordered = <String>[];
final conflicts = <(String, String), String>{};
for (final (index, result) in results.indexed) {
for (final planned in result.manualSteps) {
final step = planned.step;
final wire = step.id.wire;
final app = result.contractCase.name;
if (steps[wire] case (final first, final firstApp)) {
if (first.title != step.title) {
conflicts.putIfAbsent(
(wire, step.title),
() => '$wire: "${first.title}" in $firstApp, "${step.title}" in '
'$app',
);
}
} else {
steps[wire] = (step, app);
if (index < everyModule.length) ordered.add(wire);
}
final modules = neededBy.putIfAbsent(wire, () => {});
for (final origin in planned.contributors) {
if (origin case ModuleOrigin(:final module)) modules.add('$module');
if (origin case RoleTemplateOrigin(:final role)) {
modules.addAll([
for (final provider in result.resolution!.providersOf(role))
'${provider.id}',
]);
}
}
}
}
if (conflicts.isNotEmpty) {
throw StateError(
'Steps by hand have two titles, but the catalog has one for each '
'step:\n${conflicts.values.join('\n')}',
);
}
final others = [
for (final wire in steps.keys)
if (!ordered.contains(wire)) wire,
]..sort();
return List.unmodifiable([
for (final wire in [...ordered, ...others])
_catalogStep(steps[wire]!.$1, neededBy[wire]!),
]);
}