run method
Runs this command.
The return value is wrapped in a Future if necessary and returned by
CommandRunner.runCommand.
Implementation
@override
Future<void> run() async {
buildStartTime = DateTime.now();
// 1. Initialize Services
config = ConfigService(projectDir);
whiteLabel = WhiteLabelService(projectDir: projectDir, config: config);
cleanup = CleanupService(projectDir: projectDir, config: config);
final client = argResults?['client'] as String;
final platform = argResults?['platform'] as String;
final type = argResults?['type'] as String;
final isTest = argResults?['test'] as bool;
final enableSlack = argResults?['slack'] as bool;
String? version;
String? errorMessage;
bool buildSuccess = false;
if (enableSlack) {
slackService =
await _initializeSlackService(argResults!['slack-channel']);
}
try {
// -----------------------------------------------------------------------
// PHASE 1: VALIDATION & SETUP
// -----------------------------------------------------------------------
Logger.phase('1: Validation & Setup');
version = await _step('Reading pubspec version', () async {
return await config.getPubspecVersion();
});
await _notifyBuildStep('Build Started', client, platform, 'started',
additionalInfo: 'Version: $version, Type: $type');
final envFileName = isTest ? '.env_test' : '.env';
final envFile = File('$projectDir/clients/$client/$envFileName');
if (!envFile.existsSync()) {
throw BuildException(
'Environment file missing for client "$client": $envFileName',
fix: 'Create the missing file at "${envFile.path}".',
);
}
final envVars = await config.parseEnvFile(envFile);
await config.copyToRootEnv(envFile);
final appName = envVars['APP_NAME_PROD'];
final bundleId = envVars['BUNDLE_ID'];
final clientAssetsPath = envVars['ASSETS_PATH'];
final appIconPath = envVars['APP_ICON_PATH'];
final teamId = envVars['DEVELOPMENT_TEAM'];
_validateEnvVariables(client, envFileName, {
'APP_NAME_PROD': appName,
'BUNDLE_ID': bundleId,
'ASSETS_PATH': clientAssetsPath,
'APP_ICON_PATH': appIconPath,
});
appNameForCleanup = appName;
Logger.success('Validated configuration for "$client" ($version)');
// -----------------------------------------------------------------------
// PHASE 2: PROJECT CONFIGURATION
// -----------------------------------------------------------------------
Logger.phase('2: Project Configuration');
await _notifyBuildStep(
'Project Configuration', client, platform, 'started');
await _step('Creating backups', () async {
await config.createBackup(File('$projectDir/pubspec.yaml'));
await config
.createBackup(File('$projectDir/flutter_launcher_icons.yaml'));
final pbxprojFile =
File('$projectDir/ios/Runner.xcodeproj/project.pbxproj');
if (pbxprojFile.existsSync()) {
await config.createBackup(pbxprojFile);
}
});
await _step('Updating asset and launcher configs', () async {
await config.updateYamlValue(
File('$projectDir/flutter_launcher_icons.yaml'),
['flutter_launcher_icons', 'image_path'],
appIconPath!);
await config.updateYamlValue(File('$projectDir/pubspec.yaml'),
['splash_master', 'image'], appIconPath);
});
await _step('Syncing client branding assets & fonts', () async {
await whiteLabel.syncBrandingAssets(client, clientAssetsPath!);
await whiteLabel.applyClientFonts(clientAssetsPath);
await config.updatePubspecAssets(
clientAssetPath: client, requiredExtraAssets: ['.env']);
});
Logger.success('Project assets configured successfully');
// -----------------------------------------------------------------------
// PHASE 3: RUNNING EXTERNAL TOOLS
// -----------------------------------------------------------------------
Logger.phase('3: Running Build Commands');
await _step('Resolving dependencies (pub get)',
() => runShell('flutter pub get'));
await _step(
'Updating Bundle ID ($bundleId)',
() => runShell(
'dart run rename setBundleId --targets ios,android --value "$bundleId"'));
await _step(
'Updating App Name ($appName)',
() => runShell(
'dart run rename setAppName --targets ios,android --value "$appName"'));
await _step('Generating Launcher Icons',
() => runShell('dart run flutter_launcher_icons'));
await _step('Generating Native Splash Screens',
() => runShell('dart run splash_master create'));
await _step('Applying OS-specific splash & icon patches', () async {
//await whiteLabel.patchIosSplash(appName!);
await whiteLabel.cleanAndroidIconCache();
});
// -----------------------------------------------------------------------
// PHASE 4: THE FINAL BUILD
// -----------------------------------------------------------------------
Logger.phase('4: Building the App');
if (platform == 'android') {
final buildType = (type == 'aab') ? 'aab' : 'apk --release';
await _step(
'Building Android ($buildType)',
() => runShell(
'flutter build $buildType --dart-define=CLIENT_ENV=".env"'));
builtFile = await _step('Renaming Android artifact', () async {
return await whiteLabel.renameOutput(
clientName: client, version: version ?? '1.0.0+1', type: type);
});
} else {
if (teamId != null) {
config.updateDevelopmentTeam(projectDir, teamId: teamId);
}
await _step(
'Building iOS IPA',
() =>
runShell('flutter build ipa --dart-define=CLIENT_ENV=".env"'));
}
buildSuccess = true;
Logger.success('Build process completed successfully!');
} catch (e, s) {
buildSuccess = false;
if (e is BuildException) {
errorMessage = e.message;
Logger.error(e.message,
cause: e.fix, stackTrace: e.originalStackTrace ?? s);
} else {
errorMessage = e.toString();
Logger.error('An unexpected error stopped the build.',
cause: e, stackTrace: s);
}
await _notifyBuildStep('Build Process', client, platform, 'failed',
errorMessage: errorMessage);
rethrow;
} finally {
// -----------------------------------------------------------------------
// FINAL PHASE: CLEANUP (ALWAYS RUNS)
// -----------------------------------------------------------------------
Logger.phase('Cleaning Up Project State');
try {
await cleanup.performFullCleanup(
appNameForCleanup: appNameForCleanup, fontsWereChanged: true);
Logger.info('Project returned to default state.');
} catch (e) {
Logger.warning('Cleanup encountered an issue: $e. Run udara_cli clean');
}
final buildDuration = buildStartTime != null
? DateTime.now().difference(buildStartTime!)
: Duration.zero;
await config.appendBuildHistory(
client: client,
platform: platform,
type: type,
version: version,
success: buildSuccess,
duration: buildDuration,
errorMessage: errorMessage,
artifactPath: builtFile?.path,
);
if (slackService != null && version != null) {
await slackService!.sendBuildSummary(
client: client,
platform: platform,
type: type,
version: version,
success: buildSuccess,
buildTime: buildDuration,
errorMessage: errorMessage,
artifactFile: builtFile,
);
}
}
}