oto/lib/oto/application.dart

415 lines
13 KiB
Dart

// ignore_for_file: depend_on_referenced_packages
import 'dart:async';
import 'dart:io';
import 'package:dart_framework/log/log.dart';
import 'package:dart_framework/utils/string_util.dart';
import 'package:dart_framework/utils/system_util.dart';
import 'package:oto/cli/cli.dart';
import 'package:oto/oto/commands/command.dart';
import 'package:oto/oto/commands/command_registry.dart';
import 'package:oto/oto/core/build_result.dart';
import 'package:oto/oto/core/defined_data.dart';
import 'package:oto/oto/core/execution_context.dart';
import 'package:oto/oto/core/system_runtime.dart';
import 'package:oto/oto/pipeline/pipeline.dart';
import 'package:oto/oto/data/command_data.dart';
import 'package:oto/oto/core/data_composer.dart';
import 'package:yaml/yaml.dart';
import 'package:path/path.dart' as path;
enum BuildType { jenkins, test, file, scheduler }
enum CommandState { ready, progress, complete }
class Application {
Application._privateConstructor();
static final Application _instance = Application._privateConstructor();
static Application get instance => _instance;
static String get current {
if (Application.instance.buildType == BuildType.jenkins) {
return Platform.environment['WORKSPACE']!;
} else {
return path.current;
}
}
static File? _logFile;
static bool _logEnable = true;
static String get enter {
return _logFile == null ? '' : '\n';
}
SystemRuntime systemRuntime = const DefaultSystemRuntime();
Map<BuildType, DataComposer Function()> get _mapComposer => {
BuildType.test: () => DataComposerTest(),
BuildType.jenkins: () => DataComposerJenkins(runtime: systemRuntime),
BuildType.file: () => DataComposerFile(),
};
ExecutionContext context = ExecutionContext();
DataCommon? get commonData => context.commonData;
set commonData(DataCommon? value) => context.commonData = value;
Map<String, dynamic> get property => context.property;
set property(Map<String, dynamic> value) => context.property = value;
Map<String, CommandState> get commandStates => context.commandStates;
set commandStates(Map<String, CommandState> value) =>
context.commandStates = value;
Map<String, DataCommand> get dataCommandMap => context.dataCommandMap;
set dataCommandMap(Map<String, DataCommand> value) =>
context.dataCommandMap = value;
late Pipeline pipeline;
BuildType _buildType = BuildType.test;
BuildType get buildType {
return _buildType;
}
Future<BuildResult> build(BuildType buildType,
{String? yamlContent,
DataBuild? buildData,
bool logEnable = true,
String? logPath}) async {
_buildType = buildType;
_logEnable = logEnable;
dataCommandMap = {};
commandStates = {};
await setUTF8();
registerAllCommands();
DataBuild build;
DataComposer? composer;
// var envArgs = envArguments(arguments);
// bool isTest = envArgs.containsKey('isTest'); //for Unit Test
try {
if (_buildType == BuildType.scheduler) {
commonData = DataCommon.fromJson(FileData.jenkinsMap!);
build = buildData!;
_logFile = File(logPath!);
CLI.logFunc = log;
} else {
composer = _mapComposer[buildType]!();
await composer.compose(yamlContent, printBuildStep);
commonData = composer.commonData;
final buildMap = getMapFromYamlA(composer.buildYaml);
final mapValidate = _validateBuildMap(buildMap);
if (!mapValidate.enable) {
final ex = ExceptionData()
..phase = 'Validate build yaml'
..message = mapValidate.message;
throw Exception(ex);
}
final commandValidate =
_validateCommandList(buildMap!['commands'] as List);
if (!commandValidate.enable) {
final ex = ExceptionData()
..phase = 'Validate command list'
..message = commandValidate.message;
throw Exception(ex);
}
build = DataBuild.fromJson(buildMap);
}
if (_logEnable) {
await CLI.printString(
'$enter********************************* Build Data *************************************',
color: Color.magenta);
if (composer != null) log(composer.buildYaml);
}
property = build.property ?? {};
if (!property.containsKey('workspace')) {
property['workspace'] = current;
}
property = Command.replaceAllTagsMap(property, replace: true);
// Register commands; filter out duplicate command IDs
final commandMapValidate = _populateCommandMap(build, context);
if (!commandMapValidate.enable) {
var ex = ExceptionData();
ex.phase = 'Validate command list';
ex.message = commandMapValidate.message;
throw Exception(ex);
}
//Parse pipeline & validate
var validateResult = Pipeline.pipelineInitialize(build.pipeline!.workflow,
context: context);
if (!validateResult.enable) {
var ex = ExceptionData();
ex.phase = 'Validate Pipeline';
ex.message = validateResult.message;
throw Exception(ex);
}
pipeline = validateResult.pipeline!;
//start build
await pipeline.execute();
} catch (e, stacktrace) {
await CLI.printString(e.toString(), color: Color.redStrong);
await CLI.printString(stacktrace.toString(), color: Color.yellowStrong);
await printBuildStep('Build Failed', Color.redStrong);
return BuildResult.failure(e, stacktrace);
}
if (logEnable) {
await printBuildStep('Build Successfully Complete', Color.cyan);
}
return const BuildResult.success();
}
Future setUTF8() async {
if (systemRuntime.isWindows) {
var data = await systemRuntime.startShell(StringBuffer('chcp 65001'),
logHandler: logWithType);
await data.waitForExit();
log('Set Korean: ${data.exitCode == 0}');
}
return simpleFuture;
}
static void logWithType(String message, LogType logType) {
log(message, logType: logType);
}
static void log(String message, {LogType logType = LogType.verbose}) {
if (_logFile == null) {
print(message);
} else {
if (_logEnable || logType == LogType.error) {
_logFile?.writeAsStringSync('[${getDates()}] $message\n',
mode: FileMode.append, flush: true);
}
}
}
static _ValidateResult _validateBuildMap(Map<String, dynamic>? map) {
if (map == null) {
return _ValidateResult(false, 'Build YAML root must be a map.');
}
final property = map['property'];
if (property != null && property is! Map) {
return _ValidateResult(
false, '"property" must be a map, got: ${property.runtimeType}.');
}
if (map['commands'] == null) {
return _ValidateResult(
false, 'Build YAML is missing required section: "commands".');
}
if (map['commands'] is! List) {
return _ValidateResult(false,
'"commands" must be a list, got: ${map['commands'].runtimeType}.');
}
final pipeline = map['pipeline'];
if (pipeline == null) {
return _ValidateResult(
false, 'Build YAML is missing required section: "pipeline".');
}
if (pipeline is! Map) {
return _ValidateResult(
false, '"pipeline" must be a map, got: ${pipeline.runtimeType}.');
}
final pipelineId = pipeline['id'];
if (pipelineId is! String || pipelineId.trim().isEmpty) {
return _ValidateResult(
false, '"pipeline.id" must be a non-empty string.');
}
final workflow = pipeline['workflow'];
if (workflow == null) {
return _ValidateResult(false, '"pipeline.workflow" is missing.');
}
if (workflow is! List) {
return _ValidateResult(false,
'"pipeline.workflow" must be a list, got: ${workflow.runtimeType}.');
}
if (workflow.isEmpty) {
return _ValidateResult(
false, '"pipeline.workflow" must contain at least one task.');
}
return _ValidateResult(true, '');
}
static _ValidateResult _validateCommandList(List commandsRaw) {
for (var i = 0; i < commandsRaw.length; i++) {
final entry = commandsRaw[i];
if (entry is! Map) {
return _ValidateResult(false, 'commands[$i] must be a map.');
}
final id = entry['id'];
if (id is! String || id.trim().isEmpty) {
return _ValidateResult(
false, 'commands[$i] "id" must be a non-empty string.');
}
final type = entry['command'];
if (type is! String || type.trim().isEmpty) {
return _ValidateResult(false,
'commands[$i] (id: "$id") "command" must be a non-empty string.');
}
final typeStr = type.toString();
final matched = CommandType.values.where((e) => e.name == typeStr);
if (matched.isEmpty) {
return _ValidateResult(
false, 'commands[$i] (id: "$id") has unknown type "$typeStr".');
}
if (!Command.registeredTypes.contains(matched.first)) {
return _ValidateResult(false,
'commands[$i] (id: "$id") type "$typeStr" is not registered.');
}
}
return _ValidateResult(true, '');
}
static dynamic _yamlToPlain(dynamic value) {
if (value is YamlMap) {
return {
for (final e in value.entries) e.key.toString(): _yamlToPlain(e.value)
};
} else if (value is YamlList) {
return [for (final item in value) _yamlToPlain(item)];
}
return value;
}
static Map<String, dynamic>? getMapFromYamlA(String yamlStr) {
if (yamlStr.length <= 5) return null;
final dynamic data = loadYaml(yamlStr);
if (data is! YamlMap) return null;
return _yamlToPlain(data) as Map<String, dynamic>;
}
void updateCommandState(String target, CommandState state) {
commandStates[target] = state;
}
Future printBuildStep(String name, Color? color) async {
var message =
'''$enter*********************************************************************************************
* $name
*********************************************************************************************''';
await CLI.printString(message, color: color ?? Color.green);
return simpleFuture;
}
static YamlValidationResult validateYamlContent(String yamlContent) {
registerAllCommands();
Map<String, dynamic>? buildMap;
try {
buildMap = getMapFromYamlA(yamlContent);
} catch (e) {
return YamlValidationResult.failure(
phase: 'Parse YAML',
message: e.toString(),
);
}
final mapValidate = _validateBuildMap(buildMap);
if (!mapValidate.enable) {
return YamlValidationResult.failure(
phase: 'Validate build yaml',
message: mapValidate.message,
);
}
final commandValidate = _validateCommandList(buildMap!['commands'] as List);
if (!commandValidate.enable) {
return YamlValidationResult.failure(
phase: 'Validate command list',
message: commandValidate.message,
);
}
DataBuild build;
try {
build = DataBuild.fromJson(buildMap);
} catch (e) {
return YamlValidationResult.failure(
phase: 'Validate build yaml',
message: e.toString(),
);
}
final validationContext = ExecutionContext();
final commandMapValidate = _populateCommandMap(build, validationContext);
if (!commandMapValidate.enable) {
return YamlValidationResult.failure(
phase: 'Validate command list',
message: commandMapValidate.message,
);
}
PipelineValidateResult pipelineValidate;
try {
pipelineValidate = Pipeline.pipelineInitialize(
build.pipeline!.workflow,
context: validationContext,
);
} catch (e) {
return YamlValidationResult.failure(
phase: 'Validate Pipeline',
message: e.toString(),
);
}
if (!pipelineValidate.enable) {
return YamlValidationResult.failure(
phase: 'Validate Pipeline',
message: pipelineValidate.message ?? 'Pipeline validation failed.',
);
}
return const YamlValidationResult.success();
}
static _ValidateResult _populateCommandMap(
DataBuild build, ExecutionContext context) {
for (var command in build.commands) {
if (context.dataCommandMap.containsKey(command.id)) {
return _ValidateResult(
false,
'Duplicate command id exists: "${command.id}"',
);
} else {
context.dataCommandMap[command.id] = command;
}
}
return const _ValidateResult(true, '');
}
}
class _ValidateResult {
final bool enable;
final String message;
const _ValidateResult(this.enable, this.message);
}
class YamlValidationResult {
final bool valid;
final String phase;
final String message;
final int exitCode;
const YamlValidationResult.success()
: valid = true,
phase = 'Validate YAML',
message = 'YAML is valid.',
exitCode = 0;
const YamlValidationResult.failure({
required this.phase,
required this.message,
this.exitCode = 10,
}) : valid = false;
Map<String, dynamic> toJson() => {
'schemaVersion': 1,
'type': 'yamlValidation',
'valid': valid,
'phase': phase,
'message': message,
'exitCode': exitCode,
};
}