├── .github └── workflows │ └── versioning.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── bin └── builve.dart ├── pubspec.lock ├── pubspec.yaml └── release ├── builve-linux ├── builve-macos ├── builve-windows.exe └── builve_0.1.1_amd64.deb /.github/workflows/versioning.yml: -------------------------------------------------------------------------------- 1 | name: Auto Version Bump 2 | permissions: 3 | contents: writ 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | types: [closed] 10 | branches: 11 | - main 12 | 13 | jobs: 14 | bump-version: 15 | runs-on: ubuntu-latest 16 | if: | 17 | (github.event_name == 'push') || 18 | (github.event.pull_request.merged == true) 19 | steps: 20 | - name: Checkout Code 21 | uses: actions/checkout@v3 22 | 23 | - name: Set up Dart 24 | uses: dart-lang/setup-dart@v1 25 | with: 26 | sdk: stable 27 | 28 | - name: Install Dependencies 29 | run: dart pub get 30 | 31 | - name: Install Cider Globally 32 | run: dart pub global activate cider 33 | 34 | - name: Determine Version Bump 35 | id: determine_bump 36 | run: | 37 | # Default to patch bump 38 | BUMP_TYPE="patch" 39 | 40 | # Check PR labels (if PR exists) 41 | if [[ "${{ github.event.pull_request.labels }}" == *"major"* ]]; then 42 | BUMP_TYPE="major" 43 | elif [[ "${{ github.event.pull_request.labels }}" == *"minor"* ]]; then 44 | BUMP_TYPE="minor" 45 | fi 46 | 47 | # Check commit message (if push event) 48 | LAST_COMMIT_MSG=$(git log -1 --pretty=%B) 49 | if [[ "$LAST_COMMIT_MSG" == *"#major"* ]]; then 50 | BUMP_TYPE="major" 51 | elif [[ "$LAST_COMMIT_MSG" == *"#minor"* ]]; then 52 | BUMP_TYPE="minor" 53 | fi 54 | 55 | echo "version_type=$BUMP_TYPE" >> $GITHUB_ENV 56 | 57 | - name: Bump Version 58 | run: | 59 | dart pub global run cider bump ${{ env.version_type }} 60 | 61 | - name: Commit and Push Version Update 62 | run: | 63 | git config --global user.name "GitHub Actions" 64 | git config --global user.email "actions@github.com" 65 | git add pubspec.yaml 66 | git commit -m "Bump version [skip ci]" 67 | git push 68 | env: 69 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # https://dart.dev/guides/libraries/private-files 2 | # Created by `dart pub` 3 | .dart_tool/ 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 2 | 3 | - Initial version. 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Dawit Beyene 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Builve 2 | 3 | Builve is a command-line tool designed to simplify Flutter build processes. It allows developers to build Flutter projects and manage their build outputs efficiently. 4 | 5 | ## Features 6 | - Build Flutter projects as APK, App Bundle, or Split APKs. 7 | - Build directly from a GitHub repository URL. 8 | - Automatically rename and organize build outputs. 9 | - Specify custom project paths and output destinations. 10 | - Exclude debug APKs from the output. 11 | 12 | ## Installation 13 | 14 | ### Option 1: Install and Run from Source 15 | 1. Clone the repository: 16 | ```bash 17 | git clone https://github.com/daveragos/builve.git 18 | ``` 19 | 2. Navigate to the project directory: 20 | ```bash 21 | cd builve 22 | ``` 23 | 3. Install dependencies: 24 | ```bash 25 | dart pub get 26 | ``` 27 | 4. Activate the CLI globally: 28 | ```bash 29 | dart pub global activate --source path . 30 | ``` 31 | 5. Add Dart's global bin directory to your PATH if not already done. 32 | 33 | ### Option 2: Download Precompiled Binaries 34 | Precompiled binaries for Linux, macOS, and Windows will be available in the [Releases](https://github.com/daveragos/builve/tree/main/release) section. 35 | 36 | ## Usage 37 | 38 | 39 | ### Basic Command 40 | ```bash 41 | builve --build-type --project-path --destination 42 | builve --repo-url [other options] 43 | ``` 44 | 45 | ### Options 46 | * `--build-type` (`-b`): Type of Flutter build. Options are: 47 | - `apk`: Build a single APK. 48 | - `appbundle`: Build an Android App Bundle. 49 | - `apk-split`: Build multiple APKs, one for each ABI. 50 | 51 | * `--project-path` (`-p`): Path to the Flutter project. Defaults to the current directory. 52 | * `--repo-url` (`-r`): GitHub repository URL of the Flutter project to build. If provided, the tool will clone the repo to a temporary directory and build from there. 53 | * `--destination` (`-d`): Directory to move the build output. Defaults to the `Downloads` folder in the user's home directory. 54 | * `--verbose` (`-v`): Show additional command output. 55 | * `--help` (`-h`): Print usage information. 56 | * `--version`: Print the tool version. 57 | 58 | ### Examples 59 | 60 | 61 | #### Build a Single APK from a local project 62 | ```bash 63 | builve --build-type apk --project-path /path/to/flutter/project 64 | ``` 65 | 66 | #### Build Split APKs from a local project 67 | ```bash 68 | builve --build-type apk-split --project-path /path/to/flutter/project --destination /path/to/output 69 | ``` 70 | 71 | #### Build an App Bundle from a local project 72 | ```bash 73 | builve --build-type appbundle --project-path /path/to/flutter/project 74 | ``` 75 | 76 | #### Build a Single APK from a GitHub repo 77 | ```bash 78 | builve --build-type apk --repo-url https://github.com/username/repo 79 | ``` 80 | 81 | ## Contributing 82 | Contributions are welcome! Feel free to open issues or submit pull requests to improve the tool. 83 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the static analysis results for your project (errors, 2 | # warnings, and lints). 3 | # 4 | # This enables the 'recommended' set of lints from `package:lints`. 5 | # This set helps identify many issues that may lead to problems when running 6 | # or consuming Dart code, and enforces writing Dart using a single, idiomatic 7 | # style and format. 8 | # 9 | # If you want a smaller set of lints you can change this to specify 10 | # 'package:lints/core.yaml'. These are just the most critical lints 11 | # (the recommended set includes the core lints). 12 | # The core lints are also what is used by pub.dev for scoring packages. 13 | 14 | include: package:lints/recommended.yaml 15 | 16 | # Uncomment the following section to specify additional rules. 17 | 18 | # linter: 19 | # rules: 20 | # - camel_case_types 21 | 22 | # analyzer: 23 | # exclude: 24 | # - path/to/excluded/files/** 25 | 26 | # For more information about the core and recommended set of lints, see 27 | # https://dart.dev/go/core-lints 28 | 29 | # For additional information about configuring this file, see 30 | # https://dart.dev/guides/language/analysis-options 31 | -------------------------------------------------------------------------------- /bin/builve.dart: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dart 2 | 3 | import 'dart:io'; 4 | import 'package:args/args.dart'; 5 | import 'package:path/path.dart' as path; 6 | 7 | const String version = '0.1.0'; 8 | 9 | ArgParser buildParser() { 10 | return ArgParser() 11 | ..addFlag( 12 | 'help', 13 | abbr: 'h', 14 | negatable: false, 15 | help: 'Print this usage information.', 16 | ) 17 | ..addFlag( 18 | 'verbose', 19 | abbr: 'v', 20 | negatable: false, 21 | help: 'Show additional command output.', 22 | ) 23 | ..addFlag('version', negatable: false, help: 'Print the tool version.') 24 | ..addOption( 25 | 'project-path', 26 | abbr: 'p', 27 | help: 'Path to the Flutter project. Defaults to the current directory.', 28 | ) 29 | ..addOption( 30 | 'destination', 31 | abbr: 'd', 32 | help: 33 | 'Destination directory to move the build output. Defaults to Downloads.', 34 | ) 35 | ..addOption( 36 | 'build-type', 37 | abbr: 'b', 38 | defaultsTo: 'apk', 39 | allowed: ['apk', 'appbundle', 'apk-split'], 40 | help: 41 | 'Type of Flutter build (apk, appbundle, apk-split). Defaults to apk.', 42 | ) 43 | ..addOption( 44 | 'repo-url', 45 | abbr: 'r', 46 | help: 'GitHub repository URL of the Flutter project to build.', 47 | ); 48 | } 49 | 50 | void printUsage(ArgParser argParser) { 51 | print('Usage: builve [arguments]'); 52 | print(argParser.usage); 53 | } 54 | 55 | Future main(List arguments) async { 56 | final ArgParser argParser = buildParser(); 57 | try { 58 | final ArgResults results = argParser.parse(arguments); 59 | bool verbose = results['verbose'] as bool; 60 | 61 | // Process the parsed arguments. 62 | if (results['help'] as bool) { 63 | printUsage(argParser); 64 | return; 65 | } 66 | if (results['version'] as bool) { 67 | print('builve version: $version'); 68 | return; 69 | } 70 | 71 | // Determine if repo-url is provided 72 | final String? repoUrl = results['repo-url'] as String?; 73 | String projectPath = 74 | results['project-path'] as String? ?? Directory.current.path; 75 | Directory? tempDir; 76 | if (repoUrl != null && repoUrl.isNotEmpty) { 77 | // Create a temp directory 78 | tempDir = await Directory.systemTemp.createTemp('builve_repo_'); 79 | print('Cloning $repoUrl into ${tempDir.path} ...'); 80 | final cloneResult = 81 | await Process.run('git', ['clone', repoUrl, tempDir.path]); 82 | if (cloneResult.exitCode != 0) { 83 | print('Error: Failed to clone repository.'); 84 | print(cloneResult.stderr); 85 | return; 86 | } 87 | projectPath = tempDir.path; 88 | } 89 | 90 | final String destination = results['destination'] as String? ?? 91 | path.join( 92 | Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']!, 93 | 'Downloads', 94 | ); 95 | final String buildType = results['build-type'] as String; 96 | 97 | // Check if the project path is a Flutter project. 98 | final File pubspecFile = File(path.join(projectPath, 'pubspec.yaml')); 99 | if (!pubspecFile.existsSync()) { 100 | print('Error: The specified project path is not a Flutter project.'); 101 | if (tempDir != null) await tempDir.delete(recursive: true); 102 | return; 103 | } 104 | 105 | // Run flutter pub get before building 106 | print('Running flutter pub get...'); 107 | final pubGetResult = await Process.run('flutter', ['pub', 'get'], 108 | workingDirectory: projectPath, runInShell: true); 109 | if (pubGetResult.exitCode != 0) { 110 | print('Error: Failed to run flutter pub get.'); 111 | print(pubGetResult.stderr); 112 | if (tempDir != null) await tempDir.delete(recursive: true); 113 | return; 114 | } 115 | if (verbose) { 116 | print(pubGetResult.stdout); 117 | } 118 | 119 | // Determine the Flutter build command. 120 | List buildCommand = ['flutter', 'build']; 121 | if (buildType == 'apk') { 122 | buildCommand.add('apk'); 123 | } else if (buildType == 'appbundle') { 124 | buildCommand.add('appbundle'); 125 | } else if (buildType == 'apk-split') { 126 | buildCommand.addAll(['apk', '--split-per-abi']); 127 | } 128 | 129 | // Run the Flutter build command. 130 | print('Building $buildType...'); 131 | final ProcessResult buildResult = await Process.run( 132 | buildCommand.first, 133 | buildCommand.sublist(1), 134 | workingDirectory: projectPath, 135 | runInShell: true, 136 | ); 137 | 138 | if (buildResult.exitCode != 0) { 139 | print('Error: Failed to build $buildType.'); 140 | print(buildResult.stderr); 141 | if (tempDir != null) await tempDir.delete(recursive: true); 142 | return; 143 | } 144 | 145 | if (verbose) { 146 | print(buildResult.stdout); 147 | } 148 | 149 | // Locate and move the generated build output. 150 | if (buildType == 'apk' || buildType == 'apk-split') { 151 | final Directory apkDir = Directory( 152 | path.join(projectPath, 'build', 'app', 'outputs', 'flutter-apk'), 153 | ); 154 | 155 | if (!apkDir.existsSync()) { 156 | print('Error: APK output directory not found.'); 157 | return; 158 | } 159 | 160 | final List apkFiles = apkDir.listSync().where((file) { 161 | return file is File && file.path.endsWith('.apk'); 162 | }).toList(); 163 | 164 | if (apkFiles.isEmpty) { 165 | print('Error: No APK files found.'); 166 | return; 167 | } 168 | 169 | // Get the Flutter project name from pubspec.yaml 170 | final String pubspecContent = pubspecFile.readAsStringSync(); 171 | final RegExp nameRegex = RegExp(r'^name:\s*(\S+)', multiLine: true); 172 | final Match? nameMatch = nameRegex.firstMatch(pubspecContent); 173 | final String projectName = nameMatch?.group(1) ?? 'flutter_project'; 174 | 175 | final Directory destinationDir = Directory( 176 | path.join(destination, '${projectName}Builve'), 177 | ); 178 | if (!destinationDir.existsSync()) { 179 | destinationDir.createSync(recursive: true); 180 | } 181 | 182 | for (final apkFile in apkFiles) { 183 | final String apkFileName = path.basename(apkFile.path); 184 | 185 | // Skip debug APKs 186 | if (apkFileName.contains('debug')) { 187 | continue; 188 | } 189 | 190 | // Rename APKs 191 | String newFileName; 192 | if (buildType == 'apk-split') { 193 | final RegExp abiRegex = RegExp(r'-(arm64-v8a|armeabi-v7a|x86_64)'); 194 | final Match? abiMatch = abiRegex.firstMatch(apkFileName); 195 | final String abiSuffix = abiMatch?.group(1) ?? ''; 196 | newFileName = '${projectName}_${abiSuffix}.apk'; 197 | } else { 198 | newFileName = '${projectName}.apk'; 199 | } 200 | 201 | final String destinationPath = path.join( 202 | destinationDir.path, 203 | newFileName, 204 | ); 205 | File(apkFile.path).copySync(destinationPath); 206 | print('Moved ${path.basename(apkFile.path)} to $destinationPath'); 207 | } 208 | } else if (buildType == 'appbundle') { 209 | final String bundlePath = path.join( 210 | projectPath, 211 | 'build', 212 | 'app', 213 | 'outputs', 214 | 'bundle', 215 | 'release', 216 | 'app-release.aab', 217 | ); 218 | 219 | if (!File(bundlePath).existsSync()) { 220 | print('Error: App Bundle file not found.'); 221 | return; 222 | } 223 | 224 | // Get the Flutter project name from pubspec.yaml 225 | final String pubspecContent = pubspecFile.readAsStringSync(); 226 | final RegExp nameRegex = RegExp(r'^name:\s*(\S+)', multiLine: true); 227 | final Match? nameMatch = nameRegex.firstMatch(pubspecContent); 228 | final String projectName = nameMatch?.group(1) ?? 'flutter_project'; 229 | 230 | final Directory destinationDir = Directory( 231 | path.join(destination, '${projectName}Builve'), 232 | ); 233 | if (!destinationDir.existsSync()) { 234 | destinationDir.createSync(recursive: true); 235 | } 236 | 237 | final String destinationPath = path.join( 238 | destinationDir.path, 239 | '${projectName}.aab', 240 | ); 241 | File(bundlePath).copySync(destinationPath); 242 | 243 | print('App Bundle successfully built and moved to: $destinationPath'); 244 | } 245 | // Clean up temp directory if used 246 | if (tempDir != null) { 247 | print('Cleaning up temporary directory...'); 248 | await tempDir.delete(recursive: true); 249 | } 250 | } on FormatException catch (e) { 251 | print(e.message); 252 | print(''); 253 | printUsage(argParser); 254 | } on Exception catch (e) { 255 | print('Error: ${e.toString()}'); 256 | } on Error catch (e) { 257 | print('Error: ${e.toString()}'); 258 | } catch (e) { 259 | print('Error: ${e.toString()}'); 260 | } 261 | exit(0); 262 | } 263 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | args: 5 | dependency: "direct main" 6 | description: 7 | name: args 8 | sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "2.7.0" 12 | path: 13 | dependency: "direct main" 14 | description: 15 | name: path 16 | sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "1.9.1" 20 | sdks: 21 | dart: ">=3.4.0 <4.0.0" 22 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: builve 2 | description: A CLI tool to build Flutter APKs and move them to a specified directory. 3 | version: 0.1.1 4 | homepage: https://github.com/ragoose/builve 5 | repository: https://github.com/ragoose/builve 6 | environment: 7 | sdk: ">=3.0.0" 8 | 9 | # Add regular dependencies here. 10 | dependencies: 11 | args: ^2.5.0 12 | path: ^1.9.1 13 | 14 | executables: 15 | builve: builve -------------------------------------------------------------------------------- /release/builve-linux: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daveragos/builve/f450b801c67ed48eb393ac3c8a9c85b1cf3d0097/release/builve-linux -------------------------------------------------------------------------------- /release/builve-macos: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daveragos/builve/f450b801c67ed48eb393ac3c8a9c85b1cf3d0097/release/builve-macos -------------------------------------------------------------------------------- /release/builve-windows.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daveragos/builve/f450b801c67ed48eb393ac3c8a9c85b1cf3d0097/release/builve-windows.exe -------------------------------------------------------------------------------- /release/builve_0.1.1_amd64.deb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daveragos/builve/f450b801c67ed48eb393ac3c8a9c85b1cf3d0097/release/builve_0.1.1_amd64.deb --------------------------------------------------------------------------------