// 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 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 get property => context.property; set property(Map value) => context.property = value; Map get commandStates => context.commandStates; set commandStates(Map value) => context.commandStates = value; Map get dataCommandMap => context.dataCommandMap; set dataCommandMap(Map value) => context.dataCommandMap = value; late Pipeline pipeline; BuildType _buildType = BuildType.test; BuildType get buildType { return _buildType; } Future 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? 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? 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; } 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? 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 toJson() => { 'schemaVersion': 1, 'type': 'yamlValidation', 'valid': valid, 'phase': phase, 'message': message, 'exitCode': exitCode, }; }