in app/lib/dartdoc/dartdoc_runner.dart [196:427]
Future<JobStatus> process(Job job) async {
final packageStatus = await scoreCardBackend.getPackageStatus(
job.packageName!, job.packageVersion!);
// In case the package was deleted between scheduling and the actual delete.
if (!packageStatus.exists) {
_logger.info('Package does not exist: $job.');
return JobStatus.skipped;
}
// We know that dartdoc will fail on this package, no reason to run it.
if (packageStatus.isLegacy) {
_logger.info('Package is on legacy SDK: $job.');
await _storeScoreCard(
job,
_emptyReport(
title: 'Did not run dartdoc',
description:
'This package is not compatible with the current analysis SDK version',
));
return JobStatus.skipped;
}
// Do not check for discontinued status, we still generate documentation for
// such packages.
if (packageStatus.isObsolete) {
_logger
.info('Package is older than two years and has newer release: $job.');
await _storeScoreCard(
job,
_emptyReport(
title: 'Did not run dartdoc',
description:
'Package version is older than two years and has a newer release.',
));
return JobStatus.skipped;
}
// Detect silent errors or timeouts and make sure we unblock the seemingly
// neverending 'awaiting' statuses.
if ((job.attemptCount ?? 0) > 1) {
final currentCard = await scoreCardBackend.getScoreCardData(
job.packageName!,
job.packageVersion!,
onlyCurrent: true,
);
if (currentCard?.dartdocReport == null) {
// These package versions are worth investigating, but we don't need
// alerts on them.
_logger.warning('Missing dartdoc report when error was present: $job.');
await _storeScoreCard(
job,
_emptyReport(
title: 'Failed to run dartdoc',
description: 'The dartdoc process timed out.',
));
}
}
final logger =
Logger('pub.dartdoc.runner/${job.packageName}/${job.packageVersion}');
final tempDir =
await Directory.systemTemp.createTemp('pub-dartlang-dartdoc');
final tempDirPath = tempDir.resolveSymbolicLinksSync();
final pkgPath = p.join(tempDirPath, 'pkg');
final tarBaseDir = p.join(tempDirPath, 'output');
final dartdocContentDir =
p.join(tarBaseDir, job.packageName, job.packageVersion);
final uploadDir = p.join(tempDirPath, 'upload');
// directories need to be created
await Directory(pkgPath).create(recursive: true);
await Directory(dartdocContentDir).create(recursive: true);
await Directory(uploadDir).create(recursive: true);
final latestVersion =
await packageBackend.getLatestVersion(job.packageName!);
final bool isLatestStable = latestVersion == job.packageVersion;
bool depsResolved = false;
DartdocResult? dartdocResult;
bool hasContent = false;
PubDartdocData? dartdocData;
String reportStatus = ReportStatus.failed;
String? abortMessage;
DartdocEntry? entry;
try {
await withToolEnv(
usesPreviewSdk: packageStatus.usesPreviewAnalysisSdk,
fn: (toolEnv) async {
final sw = Stopwatch()..start();
final usesFlutter = packageStatus.usesFlutter;
final logFileOutput = StringBuffer();
logFileOutput.write('Dartdoc generation for $job\n\n'
'runtime: ${versions.runtimeVersion}\n'
'runtime Dart SDK: ${versions.runtimeSdkVersion}\n'
'dartdoc: ${versions.dartdocVersion}\n'
'pana: ${versions.panaVersion}\n'
'toolEnv Dart SDK: ${toolEnv.runtimeInfo.sdkVersion}\n'
'usesFlutter: $usesFlutter\n'
'flutter: ${toolEnv.runtimeInfo.flutterVersions}\n'
'started: ${clock.now().toUtc().toIso8601String()}\n\n');
await _runner.downloadAndExtract(
package: job.packageName!,
version: job.packageVersion!,
destination: pkgPath,
);
final uuid = createUuid();
// Resolve dependencies only for non-legacy package versions.
if (!packageStatus.isLegacy) {
final output = await _resolveDependencies(
logger, toolEnv, job, pkgPath, usesFlutter, logFileOutput);
depsResolved = output == null;
abortMessage ??= output;
} else {
logFileOutput.write(
'Package version does not allow current SDK, skipping pub upgrade.\n\n');
}
// Generate docs only for packages that have healthy dependencies.
if (depsResolved) {
dartdocResult = await _generateDocs(
toolEnv, logger, job, pkgPath, dartdocContentDir, logFileOutput,
usesPreviewSdk: packageStatus.usesPreviewAnalysisSdk);
hasContent =
dartdocResult!.hasIndexHtml && dartdocResult!.hasIndexJson;
} else {
logFileOutput
.write('Dependencies were not resolved, skipping dartdoc.\n\n');
}
if (hasContent) {
try {
await DartdocCustomizer(customizerConfig(
packageName: job.packageName!,
packageVersion: job.packageVersion!,
isLatestStable: job.isLatestStable,
)).customizeDir(dartdocContentDir);
logFileOutput.write('Content customization completed.\n\n');
} catch (e, st) {
// Do not block on customization failure.
_logger.severe('Dartdoc customization failed ($job).', e, st);
logFileOutput.write('Content customization failed.\n\n');
}
await _blob(uuid, dartdocContentDir, uploadDir, logFileOutput);
await _tar(tempDirPath, tarBaseDir, uploadDir, logFileOutput);
} else {
logFileOutput.write('No content found!\n\n');
}
entry = await _createEntry(uuid, toolEnv, job, dartdocContentDir,
uploadDir, usesFlutter, depsResolved, hasContent, sw.elapsed);
logFileOutput
.write('entry created: ${entry!.uuid} in ${sw.elapsed}\n\n');
logFileOutput
.write('completed: ${entry!.timestamp!.toIso8601String()}\n');
await _writeLog(uploadDir, logFileOutput);
},
);
final oldEntry =
await dartdocBackend.getEntry(job.packageName!, job.packageVersion!);
if (entry!.isRegression(oldEntry)) {
logger.severe('Regression detected in $job, aborting upload.');
// If `isLatest` has changed, we still want to update the old entry,
// even if the job failed. `isLatest` is used to redirect latest
// versions from versioned url to url with `/latest/`, and this
// is cheaper than checking it on each request.
if (oldEntry!.isLatest != entry!.isLatest) {
await dartdocBackend.updateOldIsLatest(oldEntry,
isLatest: entry!.isLatest);
}
} else {
await dartdocBackend.uploadDir(entry!, uploadDir);
reportStatus = hasContent ? ReportStatus.success : ReportStatus.failed;
}
if (!hasContent && isLatestStable) {
reportIssueWithLatest(job, 'No content.');
}
dartdocData = await _loadPubDartdocData(logger, dartdocContentDir);
} catch (e, st) {
reportStatus = ReportStatus.aborted;
if (isLatestStable) {
reportIssueWithLatest(job, '$e\n$st');
}
abortMessage ??=
'Running `dartdoc` failed with the following error: `$e`\n\n```\n$st\n```\n';
} finally {
await tempDir.delete(recursive: true);
}
final coverage = dartdocData?.coverage;
ReportSection documentationSection;
if (hasContent && coverage != null) {
documentationSection = documentationCoverageSection(
documented: coverage.documented,
total: coverage.total,
);
} else {
if (dartdocResult != null && dartdocResult!.wasTimeout) {
abortMessage ??= '`dartdoc` timed out.';
}
if (abortMessage == null && dartdocResult != null) {
final output =
_mergeOutput(dartdocResult!.processResult, compress: true);
abortMessage = '`dartdoc` failed with:\n\n```\n$output\n```';
}
abortMessage ??= '`dartdoc` failed with unknown reason.';
documentationSection = dartdocFailedSection(abortMessage!);
}
await _storeScoreCard(
job,
DartdocReport(
timestamp: clock.now().toUtc(),
reportStatus: reportStatus,
dartdocEntry: entry,
documentationSection: documentationSection,
));
if (abortMessage != null) {
return JobStatus.aborted;
} else {
return hasContent ? JobStatus.success : JobStatus.failed;
}
}