From 7b31deff16d74da300cb7794c4b14103a2457968 Mon Sep 17 00:00:00 2001 From: An Nguyen Date: Thu, 5 Jun 2025 09:07:15 +0700 Subject: [PATCH 1/3] feat(windows): add post-build command option for Windows packaging Add --post-package-windows-cmd option to execute custom commands between build and package steps for Windows platform. This enables custom file operations (like code signing) on the built executable before packaging. Changes: - Add new CLI option --post-package-windows-cmd in command_package.dart - Implement command execution logic in unified_distributor.dart - Add proper error handling and logging for custom command execution Example usage: fastforge package --platform windows --targets exe --post-package-windows-cmd="call ./sign-file.bat" --- .../lib/src/cli/command_package.dart | 8 +++++ .../lib/src/unified_distributor.dart | 31 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/packages/unified_distributor/lib/src/cli/command_package.dart b/packages/unified_distributor/lib/src/cli/command_package.dart index 52617bb1..200217fd 100644 --- a/packages/unified_distributor/lib/src/cli/command_package.dart +++ b/packages/unified_distributor/lib/src/cli/command_package.dart @@ -89,6 +89,12 @@ class CommandPackage extends Command { 'You may add multiple \'--build-dart-define key=value\' pairs', ].join('\n'), ); + + argParser.addOption( + 'post-package-windows-cmd', + valueHelp: 'command', + help: 'Command to execute after Windows build but before packaging (e.g., "call ./sign-file.bat")', + ); } final UnifiedDistributor distributor; @@ -115,6 +121,7 @@ class CommandPackage extends Command { final String? artifactName = argResults?['artifact-name']; final String? flutterBuildArgs = argResults?['flutter-build-args']; final bool isSkipClean = argResults?.wasParsed('skip-clean') ?? false; + final String? postPackageWindowsCmd = argResults?['post-package-windows-cmd']; final Map buildArguments = _generateBuildArgs(flutterBuildArgs); @@ -136,6 +143,7 @@ class CommandPackage extends Command { artifactName: artifactName, cleanBeforeBuild: !isSkipClean, buildArguments: buildArguments, + postPackageWindowsCmd: postPackageWindowsCmd, ); } diff --git a/packages/unified_distributor/lib/src/unified_distributor.dart b/packages/unified_distributor/lib/src/unified_distributor.dart index 707c9687..dd165a79 100644 --- a/packages/unified_distributor/lib/src/unified_distributor.dart +++ b/packages/unified_distributor/lib/src/unified_distributor.dart @@ -144,6 +144,7 @@ class UnifiedDistributor { required bool cleanBeforeBuild, required Map buildArguments, Map? variables, + String? postPackageWindowsCmd, }) async { List makeResultList = []; @@ -186,6 +187,36 @@ class UnifiedDistributor { } if (buildResult != null) { + // Execute custom Windows command if specified and platform is Windows + if (platform.toLowerCase() == 'windows' && + postPackageWindowsCmd != null && + postPackageWindowsCmd.isNotEmpty) { + logger.info('Executing post-build Windows command: $postPackageWindowsCmd'); + try { + ProcessResult result = await Process.run( + 'cmd', + ['/c', postPackageWindowsCmd], + workingDirectory: Directory.current.path, + ); + + if (result.exitCode == 0) { + logger.info('Post-build command executed successfully'.brightGreen()); + if (result.stdout.toString().isNotEmpty) { + print(result.stdout); + } + } else { + logger.severe('Post-build command failed with exit code ${result.exitCode}'.red()); + if (result.stderr.toString().isNotEmpty) { + logger.severe(result.stderr.toString().red()); + } + throw Exception('Post-build command failed'); + } + } catch (error) { + logger.severe('Failed to execute post-build command: $error'.red()); + rethrow; + } + } + String buildMode = buildArguments.containsKey('profile') ? 'profile' : 'release'; Map? arguments = { From a7c52fc00ecb57325521906b2f61ed3006faccbc Mon Sep 17 00:00:00 2001 From: An Nguyen Date: Thu, 5 Jun 2025 14:46:06 +0700 Subject: [PATCH 2/3] fix(unified_distributor): improve logging and command parsing for post-package Windows commands --- .../lib/src/unified_distributor.dart | 50 ++++++++++++++++--- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/packages/unified_distributor/lib/src/unified_distributor.dart b/packages/unified_distributor/lib/src/unified_distributor.dart index dd165a79..1af70d04 100644 --- a/packages/unified_distributor/lib/src/unified_distributor.dart +++ b/packages/unified_distributor/lib/src/unified_distributor.dart @@ -162,7 +162,7 @@ class UnifiedDistributor { BuildResult? buildResult; for (String target in targets) { - logger.info('Packaging ${pubspec.name} ${pubspec.version} as $target:'); + logger.info('Packaging ${pubspec.name} ${pubspec.version} as $target for ${platform.toLowerCase()}:'); if (!isBuildOnlyOnce || (isBuildOnlyOnce && buildResult == null)) { try { buildResult = await _builder.build( @@ -188,24 +188,57 @@ class UnifiedDistributor { if (buildResult != null) { // Execute custom Windows command if specified and platform is Windows - if (platform.toLowerCase() == 'windows' && - postPackageWindowsCmd != null && + if (platform.toLowerCase() == 'windows' && + postPackageWindowsCmd != null && postPackageWindowsCmd.isNotEmpty) { - logger.info('Executing post-build Windows command: $postPackageWindowsCmd'); + logger.info('Executing post-package Windows command: $postPackageWindowsCmd'); try { + // Parse command with proper quote handling + List cmdArguments = []; + bool inQuotes = false; + StringBuffer currentArg = StringBuffer(); + + for (int i = 0; i < postPackageWindowsCmd.length; i++) { + String char = postPackageWindowsCmd[i]; + + if (char == '"') { + // Handle escaped quotes + if (i + 1 < postPackageWindowsCmd.length && postPackageWindowsCmd[i + 1] == '"') { + currentArg.write('"'); + i++; // Skip next quote + } else { + inQuotes = !inQuotes; + } + } else if (char == ' ' && !inQuotes) { + if (currentArg.length > 0) { + cmdArguments.add(currentArg.toString()); + currentArg.clear(); + } + } else { + currentArg.write(char); + } + } + + // Add the last argument if any + if (currentArg.length > 0) { + cmdArguments.add(currentArg.toString()); + } + + logger.info('Parsed command arguments: $cmdArguments'); + ProcessResult result = await Process.run( 'cmd', - ['/c', postPackageWindowsCmd], + ['/c', ...cmdArguments], workingDirectory: Directory.current.path, ); - + if (result.exitCode == 0) { - logger.info('Post-build command executed successfully'.brightGreen()); + logger.info('Post-package command executed successfully'.brightGreen()); if (result.stdout.toString().isNotEmpty) { print(result.stdout); } } else { - logger.severe('Post-build command failed with exit code ${result.exitCode}'.red()); + logger.severe('Post-package command failed with exit code ${result.exitCode}'.red()); if (result.stderr.toString().isNotEmpty) { logger.severe(result.stderr.toString().red()); } @@ -217,6 +250,7 @@ class UnifiedDistributor { } } + String buildMode = buildArguments.containsKey('profile') ? 'profile' : 'release'; Map? arguments = { From d5d77b4d48a4b9f7cb50b0badb755153cc5db3ea Mon Sep 17 00:00:00 2001 From: An Nguyen Date: Thu, 5 Jun 2025 17:26:08 +0700 Subject: [PATCH 3/3] fix: improve Windows path handling in post-package command - Fix path escaping to prevent double backslashes in Windows paths - Add proper handling of paths with spaces in post-package commands - Improve batch file execution with enabledelayedexpansion --- .../lib/src/unified_distributor.dart | 90 ++++++++++--------- 1 file changed, 46 insertions(+), 44 deletions(-) diff --git a/packages/unified_distributor/lib/src/unified_distributor.dart b/packages/unified_distributor/lib/src/unified_distributor.dart index 1af70d04..6c021a92 100644 --- a/packages/unified_distributor/lib/src/unified_distributor.dart +++ b/packages/unified_distributor/lib/src/unified_distributor.dart @@ -193,56 +193,58 @@ class UnifiedDistributor { postPackageWindowsCmd.isNotEmpty) { logger.info('Executing post-package Windows command: $postPackageWindowsCmd'); try { - // Parse command with proper quote handling - List cmdArguments = []; - bool inQuotes = false; - StringBuffer currentArg = StringBuffer(); + // Create a temporary batch file to execute the command + final tempDir = Directory.systemTemp; + final tempFile = File('${tempDir.path}\\fastforge_temp_cmd_${DateTime.now().millisecondsSinceEpoch}.bat'); - for (int i = 0; i < postPackageWindowsCmd.length; i++) { - String char = postPackageWindowsCmd[i]; + try { + // Process the command to properly handle paths with spaces + String processedCmd = postPackageWindowsCmd; - if (char == '"') { - // Handle escaped quotes - if (i + 1 < postPackageWindowsCmd.length && postPackageWindowsCmd[i + 1] == '"') { - currentArg.write('"'); - i++; // Skip next quote - } else { - inQuotes = !inQuotes; - } - } else if (char == ' ' && !inQuotes) { - if (currentArg.length > 0) { - cmdArguments.add(currentArg.toString()); - currentArg.clear(); + // Find all quoted paths and ensure they're properly formatted + final pathRegex = RegExp(r'"([^"]+)"'); + processedCmd = processedCmd.replaceAllMapped(pathRegex, (match) { + String path = match.group(1)!; + // Remove any existing escaping to prevent double escaping + path = path.replaceAll(r'\\', r'\'); + // Ensure the path is properly formatted for Windows + path = path.replaceAll(r'/', r'\'); + return '"$path"'; + }); + + // Write the command to the batch file + tempFile.writeAsStringSync('@echo off\nsetlocal enabledelayedexpansion\n$processedCmd', flush: true); + logger.info('Created temporary batch file: ${tempFile.path}'); + logger.info('Processed command: $processedCmd'); + + // Execute the temporary batch file + ProcessResult result = await Process.run( + tempFile.path, + [], + workingDirectory: Directory.current.path, + ); + + if (result.exitCode == 0) { + logger.info('Post-package command executed successfully'.brightGreen()); + if (result.stdout.toString().isNotEmpty) { + print(result.stdout); } } else { - currentArg.write(char); - } - } - - // Add the last argument if any - if (currentArg.length > 0) { - cmdArguments.add(currentArg.toString()); - } - - logger.info('Parsed command arguments: $cmdArguments'); - - ProcessResult result = await Process.run( - 'cmd', - ['/c', ...cmdArguments], - workingDirectory: Directory.current.path, - ); - - if (result.exitCode == 0) { - logger.info('Post-package command executed successfully'.brightGreen()); - if (result.stdout.toString().isNotEmpty) { - print(result.stdout); + logger.severe('Post-package command failed with exit code ${result.exitCode}'.red()); + if (result.stderr.toString().isNotEmpty) { + logger.severe(result.stderr.toString().red()); + } + if (result.stdout.toString().isNotEmpty) { + logger.severe(result.stdout.toString().red()); + } + throw Exception('Post-build command failed'); } - } else { - logger.severe('Post-package command failed with exit code ${result.exitCode}'.red()); - if (result.stderr.toString().isNotEmpty) { - logger.severe(result.stderr.toString().red()); + } finally { + // Clean up the temporary file + if (tempFile.existsSync()) { + tempFile.deleteSync(); + logger.info('Cleaned up temporary batch file'); } - throw Exception('Post-build command failed'); } } catch (error) { logger.severe('Failed to execute post-build command: $error'.red());