├── .pubignore ├── example ├── demo_app │ ├── assets │ │ ├── images │ │ │ ├── installer.ico │ │ │ ├── installer.png │ │ │ └── installer.svg │ │ └── LICENSE.txt │ ├── windows │ │ ├── runner │ │ │ ├── resources │ │ │ │ └── app_icon.ico │ │ │ ├── resource.h │ │ │ ├── utils.h │ │ │ ├── runner.exe.manifest │ │ │ ├── flutter_window.h │ │ │ ├── main.cpp │ │ │ ├── CMakeLists.txt │ │ │ ├── utils.cpp │ │ │ ├── flutter_window.cpp │ │ │ ├── Runner.rc │ │ │ ├── win32_window.h │ │ │ └── win32_window.cpp │ │ ├── flutter │ │ │ ├── generated_plugin_registrant.cc │ │ │ ├── generated_plugin_registrant.h │ │ │ ├── generated_plugins.cmake │ │ │ └── CMakeLists.txt │ │ ├── .gitignore │ │ └── CMakeLists.txt │ ├── README.md │ ├── .gitignore │ ├── .metadata │ ├── test │ │ └── widget_test.dart │ ├── analysis_options.yaml │ ├── pubspec.yaml │ ├── lib │ │ └── main.dart │ └── pubspec.lock └── README.md ├── analysis_options.yaml ├── lib ├── inno_bundle.dart ├── models │ ├── build_tool.dart │ ├── admin_mode.dart │ ├── vcredist_mode.dart │ ├── build_type.dart │ ├── build_arch.dart │ ├── cli_config.dart │ ├── language.dart │ ├── sign_tool.dart │ ├── file_entry.dart │ └── config.dart ├── builders │ ├── installer_builder.dart │ ├── app_builder.dart │ └── script_builder.dart └── utils │ ├── constants.dart │ ├── cli_logger.dart │ ├── functions.dart │ └── installer_icon.dart ├── bin ├── build.dart ├── id.dart └── inno_bundle.dart ├── pubspec.yaml ├── .gitignore ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── test └── inno_bundle_test.dart ├── LICENSE ├── README.md └── CHANGELOG.md /.pubignore: -------------------------------------------------------------------------------- 1 | /example/demo_app/assets/images/installer.svg 2 | /example/demo_app/assets/images/installer.ico 3 | /.metadata -------------------------------------------------------------------------------- /example/demo_app/assets/images/installer.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahouari/inno_bundle/HEAD/example/demo_app/assets/images/installer.ico -------------------------------------------------------------------------------- /example/demo_app/assets/images/installer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahouari/inno_bundle/HEAD/example/demo_app/assets/images/installer.png -------------------------------------------------------------------------------- /example/demo_app/windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahouari/inno_bundle/HEAD/example/demo_app/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # Additional information about this file can be found at 2 | # https://dart.dev/guides/language/analysis-options 3 | 4 | linter: 5 | rules: 6 | avoid_print: false 7 | -------------------------------------------------------------------------------- /lib/inno_bundle.dart: -------------------------------------------------------------------------------- 1 | /// inno_bundle is not designed to be imported as library within projects, 2 | /// Rather its a CLI tool with customized output based on its configuration. 3 | library; 4 | 5 | /// only empty main. 6 | void main(List args) {} 7 | -------------------------------------------------------------------------------- /example/demo_app/windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | 10 | void RegisterPlugins(flutter::PluginRegistry* registry) { 11 | } 12 | -------------------------------------------------------------------------------- /example/demo_app/windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /example/demo_app/windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Inno bundle examples 2 | 3 | - [demo_app](https://github.com/hahouari/inno_bundle/tree/dev/example/demo_app) 4 | 5 | ## Using Shorebird (optional) 6 | 7 | You can opt into Shorebird releases for Windows by passing: 8 | 9 | ``` 10 | dart run inno_bundle --build-tool shorebird --shorebird-args "--artifact=exe" 11 | ``` 12 | 13 | Or set `build_tool: shorebird` under the `inno_bundle:` section of your config. 14 | -------------------------------------------------------------------------------- /bin/build.dart: -------------------------------------------------------------------------------- 1 | import 'package:inno_bundle/utils/cli_logger.dart'; 2 | 3 | import 'inno_bundle.dart' as build; 4 | 5 | /// Run to build installer, this is a shortcut to the main function in inno_bundle.dart 6 | /// 7 | /// This is kept for backwards compatibility. 8 | void main(List arguments) async { 9 | CliLogger.addDeferred( 10 | "This command is deprecated, use 'dart run inno_bundle' instead.", 11 | kind: CliLoggerKind.warning, 12 | ); 13 | 14 | build.main(arguments); 15 | } 16 | -------------------------------------------------------------------------------- /example/demo_app/windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: inno_bundle 2 | description: CLI tool for automating Windows installer creation using Inno Setup. 3 | maintainer: Hocine Abdellatif Houari 4 | version: 0.11.2 5 | homepage: https://github.com/hahouari/inno_bundle 6 | topics: 7 | - windows 8 | - installer 9 | - tool 10 | screenshots: 11 | - description: Inno Bundle CLI tool 12 | path: example/demo_app/assets/images/installer.png 13 | 14 | environment: 15 | sdk: ">=3.2.3 <4.0.0" 16 | 17 | platforms: 18 | windows: 19 | 20 | dependencies: 21 | args: ^2.7.0 22 | yaml: ^3.1.3 23 | uuid: ^4.5.1 24 | path: ^1.9.1 25 | 26 | dev_dependencies: 27 | test: ^1.26.3 28 | lints: ^6.0.0 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | .vs 24 | 25 | # Flutter/Dart/Pub related 26 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 27 | /pubspec.lock 28 | **/doc/api/ 29 | .dart_tool/ 30 | build/ 31 | /.metadata -------------------------------------------------------------------------------- /example/demo_app/README.md: -------------------------------------------------------------------------------- 1 | # demo_app 2 | 3 | A demo app with flutter that is bundled into exe installer using inno_bundle package. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) 13 | 14 | For help getting started with Flutter development, view the 15 | [online documentation](https://docs.flutter.dev/), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /test/inno_bundle_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:args/args.dart'; 2 | import 'package:inno_bundle/models/build_tool.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('BuildTool parsing', () { 7 | test('defaults to flutter when not provided', () { 8 | final parser = ArgParser()..addOption('build-tool'); 9 | final args = parser.parse([]); 10 | expect(BuildTool.fromArgs(args), BuildTool.flutter); 11 | }); 12 | 13 | test('parses shorebird', () { 14 | final parser = ArgParser()..addOption('build-tool'); 15 | final args = parser.parse(['--build-tool', 'shorebird']); 16 | expect(BuildTool.fromArgs(args), BuildTool.shorebird); 17 | }); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /example/demo_app/windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | -------------------------------------------------------------------------------- /example/demo_app/windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | ) 7 | 8 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 9 | ) 10 | 11 | set(PLUGIN_BUNDLED_LIBRARIES) 12 | 13 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 14 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 15 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 16 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 17 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 18 | endforeach(plugin) 19 | 20 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 21 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 22 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 23 | endforeach(ffi_plugin) 24 | -------------------------------------------------------------------------------- /example/demo_app/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Symbolication related 35 | app.*.symbols 36 | 37 | # Obfuscation related 38 | app.*.map.json 39 | 40 | # Android Studio will place build artifacts here 41 | /android/app/debug 42 | /android/app/profile 43 | /android/app/release 44 | -------------------------------------------------------------------------------- /example/demo_app/windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /bin/id.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:args/args.dart'; 4 | import 'package:inno_bundle/utils/constants.dart'; 5 | import 'package:uuid/uuid.dart'; 6 | 7 | const uuid = Uuid(); 8 | 9 | /// Run to generate an App ID (as GUID) 10 | void main(List arguments) { 11 | final parser = ArgParser() 12 | ..addOption('ns', help: "Namespace, ex: google.com") 13 | ..addFlag('hf', defaultsTo: true, help: 'Print header and footer') 14 | ..addFlag('help', abbr: 'h', negatable: false, help: 'Print help and exit'); 15 | 16 | final parsedArgs = parser.parse(arguments); 17 | final ns = parsedArgs['ns'] as String?; 18 | final hf = parsedArgs['hf'] as bool; 19 | final help = parsedArgs['help'] as bool; 20 | 21 | if (hf) print(START_MESSAGE); 22 | 23 | if (help) { 24 | print("${parser.usage}\n"); 25 | exit(0); 26 | } 27 | 28 | if (ns != null) { 29 | print(uuid.v5(Namespace.url.value, ns)); 30 | } else { 31 | print(uuid.v1()); 32 | } 33 | 34 | if (hf) print(GUID_END_MESSAGE); 35 | } 36 | -------------------------------------------------------------------------------- /lib/models/build_tool.dart: -------------------------------------------------------------------------------- 1 | /// Represents the tool used to build the Windows application. 2 | library; 3 | 4 | import 'package:args/args.dart'; 5 | 6 | /// Supported build tools. 7 | enum BuildTool { 8 | /// Use Flutter CLI to build: `flutter build windows`. 9 | flutter, 10 | 11 | /// Use Shorebird CLI to build/release: `shorebird release windows`. 12 | shorebird; 13 | 14 | /// Parse from CLI args (expects an option named `build-tool`). 15 | static BuildTool fromArgs(ArgResults args) { 16 | final value = args['build-tool'] as String?; 17 | return fromOption(value); 18 | } 19 | 20 | /// Parse from a string option (e.g., from YAML), defaults to [BuildTool.flutter]. 21 | static BuildTool fromOption(Object? option) { 22 | final s = option?.toString().toLowerCase().trim(); 23 | switch (s) { 24 | case 'shorebird': 25 | return BuildTool.shorebird; 26 | case 'flutter': 27 | case null: 28 | case '': 29 | return BuildTool.flutter; 30 | default: 31 | return BuildTool.flutter; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /example/demo_app/windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | }; 32 | 33 | #endif // RUNNER_FLUTTER_WINDOW_H_ 34 | -------------------------------------------------------------------------------- /example/demo_app/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "2e9cb0aa71a386a91f73f7088d115c0d96654829" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829 17 | base_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829 18 | - platform: windows 19 | create_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829 20 | base_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023-2025 Hocine Abdellatif Houari 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /lib/models/admin_mode.dart: -------------------------------------------------------------------------------- 1 | /// This file defines the [AdminMode] enum, which represents the different 2 | /// privilege modes that can be requested by the installer. 3 | /// 4 | /// The [AdminMode] enum provides three options: 5 | /// - [AdminMode.admin]: Requests administrator privileges. 6 | /// - [AdminMode.nonAdmin]: Does not request administrator privileges. 7 | /// - [AdminMode.auto]: Automatically determines the appropriate privilege mode. 8 | /// 9 | /// The file also includes a [fromOption] method that allows for parsing a 10 | /// configuration option (typically from a YAML or JSON configuration file) 11 | /// into one of the [AdminMode] values, ensuring compatibility with both 12 | /// boolean and string representations. 13 | library; 14 | 15 | /// An enum representing the different privilege modes to be asked by installer. 16 | enum AdminMode { 17 | admin, 18 | nonAdmin, 19 | auto; 20 | 21 | /// Parses configuration option to the desired [AdminMode]. 22 | static AdminMode fromOption(dynamic option) { 23 | if (option is bool) { 24 | return option ? admin : nonAdmin; 25 | } 26 | return auto; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /example/demo_app/assets/LICENSE.txt: -------------------------------------------------------------------------------- 1 | THIS LICENSE FILE IS ONLY FOR DEMONSTRATION PURPOSES. 2 | 3 | Copyright 2024 Hocine Abdellatif Houari 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /lib/models/vcredist_mode.dart: -------------------------------------------------------------------------------- 1 | /// This file defines the [VcRedistMode] enum, which represents the different 2 | /// modes for handling the Visual C++ Redistributable. 3 | /// 4 | /// The [VcRedistMode] enum provides three options: 5 | /// - [VcRedistMode.bundle]: Bundles the VCRedist with the installer. 6 | /// - [VcRedistMode.download]: Downloads the VCRedist after the installer closes. 7 | /// - [VcRedistMode.none]: Does not include or download the VCRedist. 8 | /// 9 | /// The file also includes a [fromOption] method that allows for parsing a 10 | /// configuration option into one of the [VcRedistMode] values, ensuring 11 | /// compatibility with boolean and string representations. 12 | library; 13 | 14 | /// An enum representing the different modes for handling the Visual C++ Redistributable. 15 | enum VcRedistMode { 16 | bundle, 17 | download, 18 | none; 19 | 20 | /// Parses configuration option to the desired [VcRedistMode]. 21 | static VcRedistMode fromOption(dynamic option) { 22 | if (option is bool) { 23 | return option ? bundle : none; 24 | } else if (option == 'download') { 25 | return download; 26 | } 27 | return none; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example/demo_app/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:demo_app/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /example/demo_app/windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "utils.h" 7 | 8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 9 | _In_ wchar_t *command_line, _In_ int show_command) { 10 | // Attach to console when present (e.g., 'flutter run') or create a 11 | // new console when running with a debugger. 12 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 13 | CreateAndAttachConsole(); 14 | } 15 | 16 | // Initialize COM, so that it is available for use in the library and/or 17 | // plugins. 18 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 19 | 20 | flutter::DartProject project(L"data"); 21 | 22 | std::vector command_line_arguments = 23 | GetCommandLineArguments(); 24 | 25 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 26 | 27 | FlutterWindow window(project); 28 | Win32Window::Point origin(10, 10); 29 | Win32Window::Size size(1280, 720); 30 | if (!window.Create(L"demo_app", origin, size)) { 31 | return EXIT_FAILURE; 32 | } 33 | window.SetQuitOnClose(true); 34 | 35 | ::MSG msg; 36 | while (::GetMessage(&msg, nullptr, 0, 0)) { 37 | ::TranslateMessage(&msg); 38 | ::DispatchMessage(&msg); 39 | } 40 | 41 | ::CoUninitialize(); 42 | return EXIT_SUCCESS; 43 | } 44 | -------------------------------------------------------------------------------- /example/demo_app/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 26 | 27 | # Additional information about this file can be found at 28 | # https://dart.dev/guides/language/analysis-options 29 | -------------------------------------------------------------------------------- /lib/models/build_type.dart: -------------------------------------------------------------------------------- 1 | /// Represents the different build types supported for the software. 2 | /// 3 | /// This enum provides three build types: 4 | /// - [release]: Represents a build type optimized for release to users. 5 | /// - [profile]: Represents a build type that allows profiling performance. 6 | /// - [debug]: Represents a build type optimized for debugging. 7 | /// 8 | /// The [dirName] getter returns the capitalized directory name associated with each build type. 9 | /// 10 | /// The [fromArgs] static method parses command-line arguments using [ArgResults] and 11 | /// determines the desired [BuildType]. If multiple flags are present, it prioritizes 12 | /// `release` over `profile` over `debug`. 13 | /// 14 | /// Example usage: 15 | /// ```dart 16 | /// var buildType = BuildType.fromArgs(argResults); 17 | /// print('Selected build type: ${buildType.dirName}'); 18 | /// ``` 19 | library; 20 | 21 | import 'package:args/args.dart'; 22 | import 'package:inno_bundle/utils/functions.dart'; 23 | 24 | /// An enum representing the different build types supported for the software. 25 | enum BuildType { 26 | release, 27 | profile, 28 | debug; 29 | 30 | /// Returns the directory name associated with the build type. 31 | String get dirName => capitalize(name); 32 | 33 | /// Parses the command-line arguments using [args] and determines the desired [BuildType]. 34 | /// 35 | /// Prioritizes `release` over `profile` over `debug` if multiple flags are present. 36 | static BuildType fromArgs(ArgResults args) { 37 | return args[debug.name] 38 | ? debug 39 | : args[profile.name] 40 | ? profile 41 | : release; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /example/demo_app/windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} WIN32 10 | "flutter_window.cpp" 11 | "main.cpp" 12 | "utils.cpp" 13 | "win32_window.cpp" 14 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 15 | "Runner.rc" 16 | "runner.exe.manifest" 17 | ) 18 | 19 | # Apply the standard set of build settings. This can be removed for applications 20 | # that need different build settings. 21 | apply_standard_settings(${BINARY_NAME}) 22 | 23 | # Add preprocessor definitions for the build version. 24 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") 25 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") 26 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") 27 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") 28 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") 29 | 30 | # Disable Windows macros that collide with C++ standard library functions. 31 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 32 | 33 | # Add dependency libraries and include directories. Add any application-specific 34 | # dependencies here. 35 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 36 | target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") 37 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 38 | 39 | # Run the Flutter tool portions of the build. This must not be removed. 40 | add_dependencies(${BINARY_NAME} flutter_assemble) 41 | -------------------------------------------------------------------------------- /example/demo_app/assets/images/installer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /example/demo_app/windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr) 51 | -1; // remove the trailing null character 52 | int input_length = (int)wcslen(utf16_string); 53 | std::string utf8_string; 54 | if (target_length <= 0 || target_length > utf8_string.max_size()) { 55 | return utf8_string; 56 | } 57 | utf8_string.resize(target_length); 58 | int converted_length = ::WideCharToMultiByte( 59 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 60 | input_length, utf8_string.data(), target_length, nullptr, nullptr); 61 | if (converted_length == 0) { 62 | return std::string(); 63 | } 64 | return utf8_string; 65 | } 66 | -------------------------------------------------------------------------------- /lib/models/build_arch.dart: -------------------------------------------------------------------------------- 1 | /// This file defines the [BuildArch] enum, which specifies the CPU architectures 2 | /// that the app is designed to run on and, consequently, the architectures supported 3 | /// by the installer generated by [inno_bundle]. 4 | /// 5 | /// The [BuildArch] enum includes two options: 6 | /// - [BuildArch.x64]: Represents the standard 64-bit architecture (`x86_64`). 7 | /// - [BuildArch.x64Compatible]: Represents a compatible 64-bit architecture (`x86_64`). 8 | /// 9 | /// The file also provides utility methods for validating and parsing configuration 10 | /// options: 11 | /// - [validateConfig]: Validates a configuration option to ensure it matches an accepted value. 12 | /// - [fromOption]: Converts a string configuration option into the corresponding [BuildArch] enum value. 13 | library; 14 | 15 | /// The CPU Architecture that the app was designed to run on, 16 | /// and therefore the architectures that are accepted by [inno_bundle] generated installer. 17 | enum BuildArch { 18 | x64("x64os", "x86_64"), 19 | x64Compatible("x64compatible", "x86_64"); 20 | 21 | /// Inno value to be inserted into the iss script. 22 | final String value; 23 | 24 | /// CPU architecture represented by this value. 25 | final String cpu; 26 | 27 | /// Creates a [BuildArch] instance with the given [value] and [cpu]. 28 | const BuildArch(this.value, this.cpu); 29 | 30 | /// List of literal string values to be accepted as valid value for option. 31 | static final acceptedStringValues = ["x64", "x64_compatible"]; 32 | 33 | /// Validate configuration option for [BuildArch]. 34 | static String? validateConfig(dynamic option, {required String configName}) { 35 | if (option == null) return null; 36 | if (option is String && acceptedStringValues.contains(option)) { 37 | return null; 38 | } 39 | return "inno_bundle.sign_tool attribute is invalid in $configName."; 40 | } 41 | 42 | /// Parses configuration option to the desired [BuildArch]. 43 | static BuildArch fromOption(String? option) { 44 | return option == 'x64' ? x64 : x64Compatible; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/builders/installer_builder.dart: -------------------------------------------------------------------------------- 1 | /// This file contains the [InstallerBuilder] class, which is responsible for 2 | /// creating the installer for the application using Inno Setup. 3 | /// 4 | /// The [InstallerBuilder] class ensures that the Inno Setup executable is properly 5 | /// installed and accessible on the system, and handles the building of the installer 6 | /// based on the provided configuration and script file. It also supports signing 7 | /// the installer if a [SignTool] is configured. 8 | library; 9 | 10 | import 'dart:io'; 11 | 12 | import 'package:inno_bundle/models/config.dart'; 13 | import 'package:inno_bundle/utils/cli_logger.dart'; 14 | import 'package:inno_bundle/utils/functions.dart'; 15 | 16 | /// A class responsible for building the installer using Inno Setup. 17 | class InstallerBuilder { 18 | /// The configuration guiding the build process. 19 | final Config config; 20 | 21 | /// The Inno Setup script file to be used for building the installer. 22 | final File scriptFile; 23 | 24 | /// Creates an instance of [InstallerBuilder] with the given [config] and [scriptFile]. 25 | const InstallerBuilder(this.config, this.scriptFile); 26 | 27 | /// Builds the installer using Inno Setup and returns the directory containing the output files. 28 | /// 29 | /// Skips the build process if [config.installer] is `false`. 30 | /// Throws a [ProcessException] if the Inno Setup process fails. 31 | Future build() async { 32 | if (!config.installer) { 33 | CliLogger.info("Skipping installer..."); 34 | return Directory(""); 35 | } 36 | 37 | final execFile = getInnoSetupExec()!; 38 | var params = [scriptFile.path]; 39 | if (config.signTool != null && config.signTool!.command.isNotEmpty) { 40 | params.add('/S${config.signTool!.name}=${config.signTool!.command}'); 41 | } 42 | 43 | final process = await Process.start( 44 | execFile.path, 45 | params, 46 | runInShell: true, 47 | workingDirectory: Directory.current.path, 48 | mode: ProcessStartMode.inheritStdio, 49 | ); 50 | final exitCode = await process.exitCode; 51 | if (exitCode != 0) exit(exitCode); 52 | return Directory.current; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /example/demo_app/windows/runner/flutter_window.cpp: -------------------------------------------------------------------------------- 1 | #include "flutter_window.h" 2 | 3 | #include 4 | 5 | #include "flutter/generated_plugin_registrant.h" 6 | 7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project) 8 | : project_(project) {} 9 | 10 | FlutterWindow::~FlutterWindow() {} 11 | 12 | bool FlutterWindow::OnCreate() { 13 | if (!Win32Window::OnCreate()) { 14 | return false; 15 | } 16 | 17 | RECT frame = GetClientArea(); 18 | 19 | // The size here must match the window dimensions to avoid unnecessary surface 20 | // creation / destruction in the startup path. 21 | flutter_controller_ = std::make_unique( 22 | frame.right - frame.left, frame.bottom - frame.top, project_); 23 | // Ensure that basic setup of the controller was successful. 24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) { 25 | return false; 26 | } 27 | RegisterPlugins(flutter_controller_->engine()); 28 | SetChildContent(flutter_controller_->view()->GetNativeWindow()); 29 | 30 | flutter_controller_->engine()->SetNextFrameCallback([&]() { 31 | this->Show(); 32 | }); 33 | 34 | // Flutter can complete the first frame before the "show window" callback is 35 | // registered. The following call ensures a frame is pending to ensure the 36 | // window is shown. It is a no-op if the first frame hasn't completed yet. 37 | flutter_controller_->ForceRedraw(); 38 | 39 | return true; 40 | } 41 | 42 | void FlutterWindow::OnDestroy() { 43 | if (flutter_controller_) { 44 | flutter_controller_ = nullptr; 45 | } 46 | 47 | Win32Window::OnDestroy(); 48 | } 49 | 50 | LRESULT 51 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 52 | WPARAM const wparam, 53 | LPARAM const lparam) noexcept { 54 | // Give Flutter, including plugins, an opportunity to handle window messages. 55 | if (flutter_controller_) { 56 | std::optional result = 57 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 58 | lparam); 59 | if (result) { 60 | return *result; 61 | } 62 | } 63 | 64 | switch (message) { 65 | case WM_FONTCHANGE: 66 | flutter_controller_->engine()->ReloadSystemFonts(); 67 | break; 68 | } 69 | 70 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 71 | } 72 | -------------------------------------------------------------------------------- /lib/models/cli_config.dart: -------------------------------------------------------------------------------- 1 | /// This file contains the [CliConfig] class, which holds the configuration 2 | /// for the command-line interface. 3 | library; 4 | 5 | import 'package:args/args.dart'; 6 | import 'package:inno_bundle/models/build_type.dart'; 7 | import 'package:inno_bundle/models/build_tool.dart'; 8 | 9 | /// A class representing the configuration for the command-line interface. 10 | class CliConfig { 11 | /// The build type (release, profile, or debug). 12 | final BuildType type; 13 | 14 | /// Whether to include the app in the installer. 15 | final bool app; 16 | 17 | /// Whether to create an installer file. 18 | final bool installer; 19 | 20 | /// Whether to install Inno Setup into your system, if not already installed. 21 | final bool installInnoSetup; 22 | 23 | /// Whether to generate app id and save it to config file. 24 | final bool generateAppId; 25 | 26 | /// Whether to generate publisher and save it to config file. 27 | /// This will use username of logged in user in machine. 28 | /// It will generate only if maintainer field is not present in config file. 29 | final bool generatePublisher; 30 | 31 | /// Namespace for the App ID (as GUID). 32 | final String? appIdNamespace; 33 | 34 | /// Arguments to be passed to flutter build. 35 | final String? buildArgs; 36 | 37 | /// Optional additional args to be passed to shorebird release. 38 | final String? shorebirdArgs; 39 | 40 | /// Selected tool to build the Windows app. 41 | final BuildTool buildTool; 42 | 43 | /// Whether `--build-tool` was explicitly provided on the CLI. 44 | final bool hasBuildToolArg; 45 | 46 | /// Override app version. 47 | final String? appVersion; 48 | 49 | /// Override sign tool name. 50 | final String? signToolName; 51 | 52 | /// Override sign tool command. 53 | final String? signToolCommand; 54 | 55 | /// Override sign tool params. 56 | final String? signToolParams; 57 | 58 | /// Creates a [CliConfig] instance with default values. 59 | const CliConfig({ 60 | this.type = BuildType.release, 61 | this.app = true, 62 | this.installer = true, 63 | this.installInnoSetup = true, 64 | this.generateAppId = true, 65 | this.generatePublisher = true, 66 | this.appIdNamespace, 67 | this.buildArgs, 68 | this.shorebirdArgs, 69 | this.appVersion, 70 | this.signToolName, 71 | this.signToolCommand, 72 | this.signToolParams, 73 | this.buildTool = BuildTool.flutter, 74 | this.hasBuildToolArg = false, 75 | }); 76 | 77 | /// Creates a [CliConfig] instance from command-line arguments using [ArgResults]. 78 | factory CliConfig.fromArgs(ArgResults args) { 79 | return CliConfig( 80 | type: BuildType.fromArgs(args), 81 | app: args['app'], 82 | installer: args['installer'], 83 | installInnoSetup: args['install-inno'], 84 | generateAppId: args['gen-app-id'], 85 | appIdNamespace: args['app-id-ns'], 86 | generatePublisher: args['gen-publisher'], 87 | buildArgs: args['build-args'], 88 | shorebirdArgs: args['shorebird-args'], 89 | appVersion: args['app-version'], 90 | signToolName: args['sign-tool-name'], 91 | signToolCommand: args['sign-tool-command'], 92 | signToolParams: args['sign-tool-params'], 93 | buildTool: BuildTool.fromArgs(args), 94 | hasBuildToolArg: args.wasParsed('build-tool'), 95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/utils/constants.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: constant_identifier_names 2 | 3 | /// Contains various constants and utility values used throughout the Inno Bundle package. 4 | /// 5 | /// This file includes: 6 | /// - Messages displayed in the CLI during different stages of the build process. 7 | /// - Paths for directories and files used during the build and installation process. 8 | /// - Globally used regular expressions. 9 | library; 10 | 11 | import 'package:inno_bundle/utils/functions.dart'; 12 | 13 | /// Start message for the CLI 14 | const String START_MESSAGE = '''\n 15 | ╔════════════════════════════════════════════════════╗ 16 | ║ ║ 17 | ║ ✨✨ INNO BUNDLE ✨✨ ║ 18 | ║ ║ 19 | ╚════════════════════════════════════════════════════╝ 20 | \n'''; 21 | 22 | /// Header message for ISS script 23 | const String scriptHeader = ''' 24 | ; Script generated by inno_bundle package for a flutter app. 25 | ; Maintainer: https://github.com/hahouari 26 | ; YOU DO NOT NEED THIS FILE AFTER YOU GENERATE THE INSTALLER. 27 | \n'''; 28 | 29 | /// End message for the CLI 30 | const String BUILD_END_MESSAGE = '''\n 31 | =========> INSTALLER GENERATED SUCCESSFULLY <========= 32 | ❤️ THANK YOU! ❤️ 33 | '''; 34 | 35 | /// End message for the CLI 36 | const String GUID_END_MESSAGE = '''\n 37 | ============> GUID GENERATED SUCCESSFULLY <=========== 38 | ❤️ THANK YOU! ❤️ 39 | '''; 40 | 41 | /// GitHub link for documentation on how to download and install Inoo Setup. 42 | const innoDownloadStepLink = 43 | "https://github.com/hahouari/inno_bundle/wiki/Install-Inno-Setup"; 44 | 45 | /// Relative build directory path for x64 windows app. 46 | const appBuildDir = ["build", "windows", "x64", "runner"]; 47 | 48 | /// Relative output directory path for x64 windows installer. 49 | const installerBuildDir = ["build", "windows", "x64", "installer"]; 50 | 51 | /// System32 directory path for MS Windows. 52 | const system32 = ["C:", "Windows", "System32"]; 53 | 54 | /// List of DLL files that must be included to avoid missing dll files issue for users. 55 | /// See https://github.com/hahouari/inno_bundle/wiki/Handling-Missing-DLL-Files. 56 | const vcDllFiles = ["msvcp140.dll", "vcruntime140.dll", "vcruntime140_1.dll"]; 57 | 58 | /// Inno Setup installation path when installed on the system level. 59 | const innoSysDirPath = ["C:", "Program Files (x86)", "Inno Setup 6"]; 60 | 61 | /// Inno Setup installation path when installed on user-specific level. 62 | final innoUserDirPath = [ 63 | getHomeDir(), 64 | "AppData", 65 | "Local", 66 | "Programs", 67 | "Inno Setup 6" 68 | ]; 69 | 70 | /// Inno Setup installation sub command for winget to install silently, 71 | /// with no upgrade, no prompts, and no UI windows. 72 | final innoSetupInstallationSubCommand = [ 73 | "install", 74 | "--id", 75 | "JRSoftware.InnoSetup", 76 | "--exact", 77 | "--version", 78 | "6.4.1", 79 | "--silent", 80 | "--no-upgrade", 81 | "--source", 82 | "winget" 83 | ]; 84 | 85 | /// Placeholder used by inno_bundle to signify that the default installer icon should be used. 86 | const defaultInstallerIconPlaceholder = "__default_installer__icon__"; 87 | 88 | /// Regex for filenames and directories. 89 | /// used to make sure some generated files do not have special characters in the names. 90 | final validFilenameRegex = RegExp(r'^[^<>:"/\\|?*\x00-\x1F]+$'); 91 | -------------------------------------------------------------------------------- /lib/models/language.dart: -------------------------------------------------------------------------------- 1 | /// Represents the different languages supported by the Inno Setup installer. 2 | /// 3 | /// Each language is associated with a specific Inno Setup language file, 4 | /// which defines the localized text and messages for the installer. 5 | /// 6 | /// Example usage: 7 | /// ```dart 8 | /// var language = Language.french; 9 | /// print(language.toInnoItem()); // Outputs: Name: "french"; MessagesFile: "compiler:Languages\\French.isl" 10 | /// ``` 11 | /// 12 | /// Properties: 13 | /// - [file]: The filename of the language-specific Inno Setup language file. 14 | /// 15 | /// Methods: 16 | /// - [getByNameOrNull]: Retrieves a [Language] instance by its name, or `null` if not found. 17 | /// - [toInnoItem]: Generates the Inno Setup language item for this language, 18 | /// formatted for inclusion in an Inno Setup script. 19 | library; 20 | 21 | /// Language enum holding every supported language that comes shipped with Inno Setup. 22 | enum Language { 23 | english("Default.isl"), 24 | armenian("Languages\\Armenian.isl"), 25 | brazilianportuguese("Languages\\BrazilianPortuguese.isl"), 26 | bulgarian("Languages\\Bulgarian.isl"), 27 | catalan("Languages\\Catalan.isl"), 28 | corsican("Languages\\Corsican.isl"), 29 | czech("Languages\\Czech.isl"), 30 | danish("Languages\\Danish.isl"), 31 | dutch("Languages\\Dutch.isl"), 32 | finnish("Languages\\Finnish.isl"), 33 | french("Languages\\French.isl"), 34 | german("Languages\\German.isl"), 35 | hebrew("Languages\\Hebrew.isl"), 36 | hungarian("Languages\\Hungarian.isl"), 37 | icelandic("Languages\\Icelandic.isl"), 38 | italian("Languages\\Italian.isl"), 39 | japanese("Languages\\Japanese.isl"), 40 | korean("Languages\\Korean.isl"), 41 | norwegian("Languages\\Norwegian.isl"), 42 | polish("Languages\\Polish.isl"), 43 | portuguese("Languages\\Portuguese.isl"), 44 | russian("Languages\\Russian.isl"), 45 | slovak("Languages\\Slovak.isl"), 46 | slovenian("Languages\\Slovenian.isl"), 47 | spanish("Languages\\Spanish.isl"), 48 | turkish("Languages\\Turkish.isl"), 49 | ukrainian("Languages\\Ukrainian.isl"), 50 | swedish("Languages\\Swedish.isl"), 51 | tamil("Languages\\Tamil.isl"), 52 | arabic("Languages\\Arabic.isl"); 53 | 54 | /// The filename of the language-specific Inno Setup language file. 55 | final String file; 56 | 57 | /// Creates a [Language] instance with the associated [file] name. 58 | const Language(this.file); 59 | 60 | /// Retrieves a [Language] instance by its name, or `null` if not found. 61 | static Language? getByNameOrNull(String name) { 62 | final index = values.indexWhere((l) => l.name == name); 63 | return index != -1 ? values[index] : null; 64 | } 65 | 66 | /// Generates the Inno Setup language item for this language. 67 | String get innoEntry { 68 | return "Name: \"$name\"; MessagesFile: \"compiler:$file\""; 69 | } 70 | 71 | /// Validate configuration option for [Language]. 72 | static String? validateConfig(dynamic option, {required String configName}) { 73 | if (option == null) return null; 74 | if (option is! String) { 75 | return "an entry in inno_bundle.languages attribute is invalid " 76 | "in $configName, expected a string, got $option."; 77 | } 78 | final language = Language.getByNameOrNull(option); 79 | if (language == null) { 80 | return "an entry in inno_bundle.languages attribute is invalid " 81 | "in $configName, language `$option` is not supported."; 82 | } 83 | return null; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Inno Bundle 2 | 3 | [![pub package](https://img.shields.io/pub/v/inno_bundle.svg)](https://pub.dev/packages/inno_bundle) 4 | [![inno setup](https://img.shields.io/badge/Inno_Setup-v6.4.1-blue)](https://jrsoftware.org/isinfo.php) 5 | ![hahouari-inno-setup](https://img.shields.io/badge/@hahouari-Inno_Setup-blue) 6 | 7 | CLI tool that simplifies bundling flutter apps into Windows installers using Inno Setup. 8 | 9 | # Guide 10 | 11 | ## 1. Install `inno_bundle` package into your project 12 | 13 | ```sh 14 | dart pub add dev:inno_bundle 15 | ``` 16 | 17 | ## 2. Build the Installer 18 | 19 | Run the build command on release mode 20 | 21 | ```sh 22 | dart run inno_bundle 23 | ``` 24 | 25 | **Note:** This will generate the initial configuration if not present in your `pubspec.yaml`. 26 | 27 | ### Use Shorebird for the app build (optional) 28 | 29 | To build using Shorebird instead of Flutter (enables code-push workflows): 30 | 31 | ```sh 32 | dart run inno_bundle --build-tool shorebird 33 | ``` 34 | 35 | Pass extra Shorebird args if needed (example): 36 | 37 | ```sh 38 | dart run inno_bundle --build-tool shorebird --shorebird-args "--artifact=exe" 39 | ``` 40 | 41 | # More Options and Examples 42 | 43 | You can find detailed documentation on customizing `inno_bundle`, including examples, on the [GitHub wiki pages](https://github.com/hahouari/inno_bundle/wiki). 44 | 45 | To configure your installer, see [Configuration Options](https://github.com/hahouari/inno_bundle/wiki/Configuration-Options), and if you need other use cases with our CLI tool, look up [CLI Options](https://github.com/hahouari/inno_bundle/wiki/CLI-Tool-Options). 46 | 47 | # Using GitHub Workflow? 48 | 49 | To automate the process of building the installer with GitHub Actions, refer to [this demo](https://github.com/hahouari/flutter_inno_workflows_demo). 50 | 51 | You can copy the [build.yaml](https://github.com/hahouari/flutter_inno_workflows_demo/blob/dev/.github/workflows/build.yaml) file into your project and make sure to update [the push branch](https://github.com/hahouari/flutter_inno_workflows_demo/blob/fb49da23996161acc80f0e9f4c169a01908a29a7/.github/workflows/build.yaml#L5). This setup will build the installer and publish it to [GitHub Releases](https://github.com/hahouari/flutter_inno_workflows_demo/releases) with the appropriate versioning. 52 | 53 | # DLL Files Handling 54 | 55 | `inno_bundle` handles including all necessary DLL files within the installer. For more info, refer to [this page](https://github.com/hahouari/inno_bundle/wiki/Handling-Missing-DLL-Files). 56 | 57 | # Reporting Issues 58 | 59 | If you encounter any issues please report them here. 60 | 61 | ## Shorebird support (optional) 62 | 63 | By default, `inno_bundle` uses the Flutter CLI to build your app (`flutter build windows`). You can opt into Shorebird releases to enable code-push based workflows. 64 | 65 | - CLI flags: 66 | - `--build-tool shorebird` to use Shorebird 67 | - `--shorebird-args "--artifact=exe --staged"` to pass extra args to `shorebird release windows` 68 | - YAML config (`inno_bundle.yaml` or `pubspec.yaml` under `inno_bundle`): 69 | - `build_tool: shorebird` 70 | 71 | Commands executed: 72 | 73 | - Shorebird: `shorebird release windows [--debug|--profile] ` 74 | - Flutter (default): `flutter build windows ./lib/main.dart -- --obfuscate --split-debug-info=build/obfuscate --build-name --build-number ` 75 | 76 | This is non-breaking; existing usage requires no changes. 77 | -------------------------------------------------------------------------------- /example/demo_app/windows/runner/Runner.rc: -------------------------------------------------------------------------------- 1 | // Microsoft Visual C++ generated resource script. 2 | // 3 | #pragma code_page(65001) 4 | #include "resource.h" 5 | 6 | #define APSTUDIO_READONLY_SYMBOLS 7 | ///////////////////////////////////////////////////////////////////////////// 8 | // 9 | // Generated from the TEXTINCLUDE 2 resource. 10 | // 11 | #include "winres.h" 12 | 13 | ///////////////////////////////////////////////////////////////////////////// 14 | #undef APSTUDIO_READONLY_SYMBOLS 15 | 16 | ///////////////////////////////////////////////////////////////////////////// 17 | // English (United States) resources 18 | 19 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) 20 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US 21 | 22 | #ifdef APSTUDIO_INVOKED 23 | ///////////////////////////////////////////////////////////////////////////// 24 | // 25 | // TEXTINCLUDE 26 | // 27 | 28 | 1 TEXTINCLUDE 29 | BEGIN 30 | "resource.h\0" 31 | END 32 | 33 | 2 TEXTINCLUDE 34 | BEGIN 35 | "#include ""winres.h""\r\n" 36 | "\0" 37 | END 38 | 39 | 3 TEXTINCLUDE 40 | BEGIN 41 | "\r\n" 42 | "\0" 43 | END 44 | 45 | #endif // APSTUDIO_INVOKED 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////////// 49 | // 50 | // Icon 51 | // 52 | 53 | // Icon with lowest ID value placed first to ensure application icon 54 | // remains consistent on all systems. 55 | IDI_APP_ICON ICON "resources\\app_icon.ico" 56 | 57 | 58 | ///////////////////////////////////////////////////////////////////////////// 59 | // 60 | // Version 61 | // 62 | 63 | #if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) 64 | #define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD 65 | #else 66 | #define VERSION_AS_NUMBER 1,0,0,0 67 | #endif 68 | 69 | #if defined(FLUTTER_VERSION) 70 | #define VERSION_AS_STRING FLUTTER_VERSION 71 | #else 72 | #define VERSION_AS_STRING "1.0.0" 73 | #endif 74 | 75 | VS_VERSION_INFO VERSIONINFO 76 | FILEVERSION VERSION_AS_NUMBER 77 | PRODUCTVERSION VERSION_AS_NUMBER 78 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 79 | #ifdef _DEBUG 80 | FILEFLAGS VS_FF_DEBUG 81 | #else 82 | FILEFLAGS 0x0L 83 | #endif 84 | FILEOS VOS__WINDOWS32 85 | FILETYPE VFT_APP 86 | FILESUBTYPE 0x0L 87 | BEGIN 88 | BLOCK "StringFileInfo" 89 | BEGIN 90 | BLOCK "040904e4" 91 | BEGIN 92 | VALUE "CompanyName", "com.hahouari" "\0" 93 | VALUE "FileDescription", "demo_app" "\0" 94 | VALUE "FileVersion", VERSION_AS_STRING "\0" 95 | VALUE "InternalName", "demo_app" "\0" 96 | VALUE "LegalCopyright", "Copyright (C) 2023 com.hahouari. All rights reserved." "\0" 97 | VALUE "OriginalFilename", "demo_app.exe" "\0" 98 | VALUE "ProductName", "demo_app" "\0" 99 | VALUE "ProductVersion", VERSION_AS_STRING "\0" 100 | END 101 | END 102 | BLOCK "VarFileInfo" 103 | BEGIN 104 | VALUE "Translation", 0x409, 1252 105 | END 106 | END 107 | 108 | #endif // English (United States) resources 109 | ///////////////////////////////////////////////////////////////////////////// 110 | 111 | 112 | 113 | #ifndef APSTUDIO_INVOKED 114 | ///////////////////////////////////////////////////////////////////////////// 115 | // 116 | // Generated from the TEXTINCLUDE 3 resource. 117 | // 118 | 119 | 120 | ///////////////////////////////////////////////////////////////////////////// 121 | #endif // not APSTUDIO_INVOKED 122 | -------------------------------------------------------------------------------- /example/demo_app/windows/runner/win32_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_WIN32_WINDOW_H_ 2 | #define RUNNER_WIN32_WINDOW_H_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | // A class abstraction for a high DPI-aware Win32 Window. Intended to be 11 | // inherited from by classes that wish to specialize with custom 12 | // rendering and input handling 13 | class Win32Window { 14 | public: 15 | struct Point { 16 | unsigned int x; 17 | unsigned int y; 18 | Point(unsigned int x, unsigned int y) : x(x), y(y) {} 19 | }; 20 | 21 | struct Size { 22 | unsigned int width; 23 | unsigned int height; 24 | Size(unsigned int width, unsigned int height) 25 | : width(width), height(height) {} 26 | }; 27 | 28 | Win32Window(); 29 | virtual ~Win32Window(); 30 | 31 | // Creates a win32 window with |title| that is positioned and sized using 32 | // |origin| and |size|. New windows are created on the default monitor. Window 33 | // sizes are specified to the OS in physical pixels, hence to ensure a 34 | // consistent size this function will scale the inputted width and height as 35 | // as appropriate for the default monitor. The window is invisible until 36 | // |Show| is called. Returns true if the window was created successfully. 37 | bool Create(const std::wstring& title, const Point& origin, const Size& size); 38 | 39 | // Show the current window. Returns true if the window was successfully shown. 40 | bool Show(); 41 | 42 | // Release OS resources associated with window. 43 | void Destroy(); 44 | 45 | // Inserts |content| into the window tree. 46 | void SetChildContent(HWND content); 47 | 48 | // Returns the backing Window handle to enable clients to set icon and other 49 | // window properties. Returns nullptr if the window has been destroyed. 50 | HWND GetHandle(); 51 | 52 | // If true, closing this window will quit the application. 53 | void SetQuitOnClose(bool quit_on_close); 54 | 55 | // Return a RECT representing the bounds of the current client area. 56 | RECT GetClientArea(); 57 | 58 | protected: 59 | // Processes and route salient window messages for mouse handling, 60 | // size change and DPI. Delegates handling of these to member overloads that 61 | // inheriting classes can handle. 62 | virtual LRESULT MessageHandler(HWND window, 63 | UINT const message, 64 | WPARAM const wparam, 65 | LPARAM const lparam) noexcept; 66 | 67 | // Called when CreateAndShow is called, allowing subclass window-related 68 | // setup. Subclasses should return false if setup fails. 69 | virtual bool OnCreate(); 70 | 71 | // Called when Destroy is called. 72 | virtual void OnDestroy(); 73 | 74 | private: 75 | friend class WindowClassRegistrar; 76 | 77 | // OS callback called by message pump. Handles the WM_NCCREATE message which 78 | // is passed when the non-client area is being created and enables automatic 79 | // non-client DPI scaling so that the non-client area automatically 80 | // responds to changes in DPI. All other messages are handled by 81 | // MessageHandler. 82 | static LRESULT CALLBACK WndProc(HWND const window, 83 | UINT const message, 84 | WPARAM const wparam, 85 | LPARAM const lparam) noexcept; 86 | 87 | // Retrieves a class instance pointer for |window| 88 | static Win32Window* GetThisFromHandle(HWND const window) noexcept; 89 | 90 | // Update the window frame's theme to match the system theme. 91 | static void UpdateTheme(HWND const window); 92 | 93 | bool quit_on_close_ = false; 94 | 95 | // window handle for top level window. 96 | HWND window_handle_ = nullptr; 97 | 98 | // window handle for hosted content. 99 | HWND child_content_ = nullptr; 100 | }; 101 | 102 | #endif // RUNNER_WIN32_WINDOW_H_ 103 | -------------------------------------------------------------------------------- /example/demo_app/windows/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.14) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") 12 | 13 | # Set fallback configurations for older versions of the flutter tool. 14 | if (NOT DEFINED FLUTTER_TARGET_PLATFORM) 15 | set(FLUTTER_TARGET_PLATFORM "windows-x64") 16 | endif() 17 | 18 | # === Flutter Library === 19 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") 20 | 21 | # Published to parent scope for install step. 22 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 23 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 24 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 25 | set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) 26 | 27 | list(APPEND FLUTTER_LIBRARY_HEADERS 28 | "flutter_export.h" 29 | "flutter_windows.h" 30 | "flutter_messenger.h" 31 | "flutter_plugin_registrar.h" 32 | "flutter_texture_registrar.h" 33 | ) 34 | list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") 35 | add_library(flutter INTERFACE) 36 | target_include_directories(flutter INTERFACE 37 | "${EPHEMERAL_DIR}" 38 | ) 39 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") 40 | add_dependencies(flutter flutter_assemble) 41 | 42 | # === Wrapper === 43 | list(APPEND CPP_WRAPPER_SOURCES_CORE 44 | "core_implementations.cc" 45 | "standard_codec.cc" 46 | ) 47 | list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") 48 | list(APPEND CPP_WRAPPER_SOURCES_PLUGIN 49 | "plugin_registrar.cc" 50 | ) 51 | list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") 52 | list(APPEND CPP_WRAPPER_SOURCES_APP 53 | "flutter_engine.cc" 54 | "flutter_view_controller.cc" 55 | ) 56 | list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") 57 | 58 | # Wrapper sources needed for a plugin. 59 | add_library(flutter_wrapper_plugin STATIC 60 | ${CPP_WRAPPER_SOURCES_CORE} 61 | ${CPP_WRAPPER_SOURCES_PLUGIN} 62 | ) 63 | apply_standard_settings(flutter_wrapper_plugin) 64 | set_target_properties(flutter_wrapper_plugin PROPERTIES 65 | POSITION_INDEPENDENT_CODE ON) 66 | set_target_properties(flutter_wrapper_plugin PROPERTIES 67 | CXX_VISIBILITY_PRESET hidden) 68 | target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) 69 | target_include_directories(flutter_wrapper_plugin PUBLIC 70 | "${WRAPPER_ROOT}/include" 71 | ) 72 | add_dependencies(flutter_wrapper_plugin flutter_assemble) 73 | 74 | # Wrapper sources needed for the runner. 75 | add_library(flutter_wrapper_app STATIC 76 | ${CPP_WRAPPER_SOURCES_CORE} 77 | ${CPP_WRAPPER_SOURCES_APP} 78 | ) 79 | apply_standard_settings(flutter_wrapper_app) 80 | target_link_libraries(flutter_wrapper_app PUBLIC flutter) 81 | target_include_directories(flutter_wrapper_app PUBLIC 82 | "${WRAPPER_ROOT}/include" 83 | ) 84 | add_dependencies(flutter_wrapper_app flutter_assemble) 85 | 86 | # === Flutter tool backend === 87 | # _phony_ is a non-existent file to force this command to run every time, 88 | # since currently there's no way to get a full input/output list from the 89 | # flutter tool. 90 | set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") 91 | set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) 92 | add_custom_command( 93 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 94 | ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} 95 | ${CPP_WRAPPER_SOURCES_APP} 96 | ${PHONY_OUTPUT} 97 | COMMAND ${CMAKE_COMMAND} -E env 98 | ${FLUTTER_TOOL_ENVIRONMENT} 99 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" 100 | ${FLUTTER_TARGET_PLATFORM} $ 101 | VERBATIM 102 | ) 103 | add_custom_target(flutter_assemble DEPENDS 104 | "${FLUTTER_LIBRARY}" 105 | ${FLUTTER_LIBRARY_HEADERS} 106 | ${CPP_WRAPPER_SOURCES_CORE} 107 | ${CPP_WRAPPER_SOURCES_PLUGIN} 108 | ${CPP_WRAPPER_SOURCES_APP} 109 | ) 110 | -------------------------------------------------------------------------------- /lib/builders/app_builder.dart: -------------------------------------------------------------------------------- 1 | /// This file contains the [AppBuilder] class, which is responsible for 2 | /// orchestrating the build process of the app using a provided configuration. 3 | /// 4 | /// The [AppBuilder] class uses the [Config] model to determine the build 5 | /// parameters and executes the Flutter build command accordingly. It also 6 | /// manages conditions such as skipping the build if a valid build already 7 | /// exists. 8 | library; 9 | 10 | import 'dart:io'; 11 | 12 | import 'package:inno_bundle/models/config.dart'; 13 | import 'package:inno_bundle/models/build_tool.dart'; 14 | import 'package:inno_bundle/models/build_type.dart'; 15 | import 'package:inno_bundle/utils/constants.dart'; 16 | import 'package:path/path.dart' as p; 17 | import 'package:inno_bundle/utils/cli_logger.dart'; 18 | 19 | /// A class responsible for building the app based on the provided configuration. 20 | class AppBuilder { 21 | /// Configuration guiding the build process. 22 | final Config config; 23 | 24 | /// Creates an instance of [AppBuilder] with the given [config]. 25 | AppBuilder(this.config); 26 | 27 | /// Builds the app using Flutter and returns the path to the build directory. 28 | /// 29 | /// If [config.app] is `false` and a valid build already exists, it skips the 30 | /// build process and returns the existing directory. Otherwise, it executes 31 | /// the Flutter build command and returns the newly generated build directory. 32 | Future build() async { 33 | final buildDirPath = p.joinAll([ 34 | Directory.current.path, 35 | ...appBuildDir, 36 | config.type.dirName, 37 | ]); 38 | final buildDir = Directory(buildDirPath); 39 | final versionParts = config.version.split("+"); 40 | final buildName = versionParts[0]; 41 | final buildNumber = 42 | versionParts.length == 1 ? "1" : versionParts.sublist(1).join("+"); 43 | 44 | if (!config.app) { 45 | if (!buildDir.existsSync() || buildDir.listSync().isEmpty) { 46 | CliLogger.warning( 47 | "${config.type.dirName} build is not available, " 48 | "--no-app is ignored.", 49 | ); 50 | } else { 51 | CliLogger.info("Skipping app..."); 52 | return buildDir; 53 | } 54 | } 55 | 56 | // Choose build tool 57 | final bool useShorebird = config.buildTool == BuildTool.shorebird; 58 | 59 | Process process; 60 | if (useShorebird) { 61 | // Build using Shorebird: shorebird release windows ... 62 | final shorebirdArgs = [ 63 | 'release', 64 | 'windows', 65 | if (config.type != BuildType.release) '--${config.type.name}', 66 | // users will typically set version via pubspec; shorebird uses app_version internally for releases if needed 67 | ]; 68 | 69 | // Append any user-specified args string as a single token to match existing style 70 | final extra = config.shorebirdArgs; 71 | if (extra != null && extra.isNotEmpty) { 72 | shorebirdArgs.add(extra); 73 | } 74 | 75 | process = await Process.start( 76 | 'shorebird', 77 | shorebirdArgs, 78 | runInShell: true, 79 | workingDirectory: Directory.current.path, 80 | mode: ProcessStartMode.inheritStdio, 81 | ); 82 | } else { 83 | // Default: Build using Flutter 84 | final flutterArgs = [ 85 | 'build', 86 | 'windows', 87 | './lib/main.dart', 88 | '--${config.type.name}', 89 | '--obfuscate', 90 | '--split-debug-info=build/obfuscate', 91 | '--build-name', 92 | buildName, 93 | '--build-number', 94 | buildNumber, 95 | ]; 96 | 97 | final extra = config.buildArgs; 98 | if (extra != null && extra.isNotEmpty) { 99 | flutterArgs.add(extra); 100 | } 101 | 102 | process = await Process.start( 103 | 'flutter', 104 | flutterArgs, 105 | runInShell: true, 106 | workingDirectory: Directory.current.path, 107 | mode: ProcessStartMode.inheritStdio, 108 | ); 109 | } 110 | 111 | final exitCode = await process.exitCode; 112 | 113 | if (exitCode != 0) exit(exitCode); 114 | return buildDir; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lib/utils/cli_logger.dart: -------------------------------------------------------------------------------- 1 | /// Provides a CLI logging utility with various log levels and colored output. 2 | /// 3 | /// The `CliLogger` class offers methods for logging messages with different severity levels, 4 | /// including `info`, `error`, `warning`, `success`, and `sLink` for logging links. It supports 5 | /// customizable log levels and provides color-coded output using ANSI escape codes. 6 | library; 7 | 8 | import 'dart:io'; 9 | 10 | /// Log levels 11 | enum CliLoggerLevel { 12 | /// Level one 13 | one, 14 | 15 | /// Level two 16 | two, 17 | 18 | /// Level three 19 | three, 20 | } 21 | 22 | /// Class holding the deferred message properties. 23 | class DeferredMessage { 24 | /// The message text. 25 | final String text; 26 | 27 | /// The message kind. 28 | final CliLoggerKind kind; 29 | 30 | /// Creates a [DeferredMessage] instance with the given properties. 31 | const DeferredMessage(this.text, this.kind); 32 | } 33 | 34 | // Reset: \x1B[0m 35 | // Black: \x1B[30m 36 | // White: \x1B[37m 37 | // Red: \x1B[31m 38 | // Green: \x1B[32m 39 | // Yellow: \x1B[33m 40 | // Blue: \x1B[34m 41 | // Cyan: \x1B[36m 42 | 43 | /// Cli Logger 44 | class CliLogger { 45 | /// Static list of deferred messages. 46 | static final List _deferredMessages = []; 47 | 48 | /// Log info 49 | static void info( 50 | String message, { 51 | CliLoggerLevel level = CliLoggerLevel.one, 52 | }) { 53 | final space = _getSpace(level); 54 | print('\x1B[34m$space🌱 $message\x1B[0m'); 55 | } 56 | 57 | /// Logs a error message at the given level. 58 | static void error( 59 | String message, { 60 | CliLoggerLevel level = CliLoggerLevel.one, 61 | }) { 62 | final space = _getSpace(level); 63 | print('$space❌ $message'); 64 | } 65 | 66 | /// Logs a error message at the given level and exits with given code. 67 | static void exitError( 68 | String message, { 69 | CliLoggerLevel level = CliLoggerLevel.one, 70 | int exitCode = 1, 71 | }) { 72 | CliLogger.error(message, level: level); 73 | exit(exitCode); 74 | } 75 | 76 | /// Logs a warning message at the given level. 77 | static void warning( 78 | String message, { 79 | CliLoggerLevel level = CliLoggerLevel.one, 80 | }) { 81 | final space = _getSpace(level); 82 | print('\x1B[33m$space🚧 $message\x1B[0m'); 83 | } 84 | 85 | /// Logs a success message at the given level. 86 | static void success( 87 | String message, { 88 | CliLoggerLevel level = CliLoggerLevel.one, 89 | }) { 90 | final space = _getSpace(level); 91 | print('\x1B[32m$space✅ $message\x1B[0m'); 92 | } 93 | 94 | /// Logs a link as a underlined text. 95 | static String sLink( 96 | String link, { 97 | CliLoggerLevel level = CliLoggerLevel.one, 98 | }) { 99 | final space = _getSpace(level); 100 | return '\x1B[34m$space🔗 $link\x1B[0m'; 101 | } 102 | 103 | static String _getSpace(CliLoggerLevel level) { 104 | var space = ''; 105 | switch (level) { 106 | case CliLoggerLevel.one: 107 | space = ''; 108 | break; 109 | case CliLoggerLevel.two: 110 | space = ' '; 111 | break; 112 | case CliLoggerLevel.three: 113 | space = ' '; 114 | break; 115 | } 116 | return space; 117 | } 118 | 119 | /// Adds a message to the list of deferred messages. 120 | static void addDeferred(String message, {required CliLoggerKind kind}) { 121 | _deferredMessages.add(DeferredMessage(message, kind)); 122 | } 123 | 124 | /// Prints all deferred messages and clears the list. 125 | static void flushDeferred() { 126 | // Print empty line to separate deferred messages from the rest of the output 127 | if (_deferredMessages.isNotEmpty) print(""); 128 | 129 | // Print all deferred messages 130 | for (final message in _deferredMessages) { 131 | switch (message.kind) { 132 | case CliLoggerKind.warning: 133 | warning(message.text); 134 | break; 135 | case CliLoggerKind.error: 136 | error(message.text); 137 | break; 138 | case CliLoggerKind.success: 139 | success(message.text); 140 | break; 141 | default: 142 | info(message.text); 143 | break; 144 | } 145 | } 146 | 147 | // Clear the list of deferred messages 148 | _deferredMessages.clear(); 149 | } 150 | } 151 | 152 | /// Enum for the different logging kinds. 153 | enum CliLoggerKind { 154 | info, 155 | warning, 156 | error, 157 | success, 158 | } 159 | -------------------------------------------------------------------------------- /example/demo_app/windows/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Project-level configuration. 2 | cmake_minimum_required(VERSION 3.14) 3 | project(demo_app LANGUAGES CXX) 4 | 5 | # The name of the executable created for the application. Change this to change 6 | # the on-disk name of your application. 7 | set(BINARY_NAME "demo_app") 8 | 9 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent 10 | # versions of CMake. 11 | cmake_policy(VERSION 3.14...3.25) 12 | 13 | # Define build configuration option. 14 | get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) 15 | if(IS_MULTICONFIG) 16 | set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" 17 | CACHE STRING "" FORCE) 18 | else() 19 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 20 | set(CMAKE_BUILD_TYPE "Debug" CACHE 21 | STRING "Flutter build mode" FORCE) 22 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 23 | "Debug" "Profile" "Release") 24 | endif() 25 | endif() 26 | # Define settings for the Profile build mode. 27 | set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") 28 | set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") 29 | set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") 30 | set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") 31 | 32 | # Use Unicode for all projects. 33 | add_definitions(-DUNICODE -D_UNICODE) 34 | 35 | # Compilation settings that should be applied to most targets. 36 | # 37 | # Be cautious about adding new options here, as plugins use this function by 38 | # default. In most cases, you should add new options to specific targets instead 39 | # of modifying this function. 40 | function(APPLY_STANDARD_SETTINGS TARGET) 41 | target_compile_features(${TARGET} PUBLIC cxx_std_17) 42 | target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") 43 | target_compile_options(${TARGET} PRIVATE /EHsc) 44 | target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") 45 | target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") 46 | endfunction() 47 | 48 | # Flutter library and tool build rules. 49 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 50 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 51 | 52 | # Application build; see runner/CMakeLists.txt. 53 | add_subdirectory("runner") 54 | 55 | 56 | # Generated plugin build rules, which manage building the plugins and adding 57 | # them to the application. 58 | include(flutter/generated_plugins.cmake) 59 | 60 | 61 | # === Installation === 62 | # Support files are copied into place next to the executable, so that it can 63 | # run in place. This is done instead of making a separate bundle (as on Linux) 64 | # so that building and running from within Visual Studio will work. 65 | set(BUILD_BUNDLE_DIR "$") 66 | # Make the "install" step default, as it's required to run. 67 | set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) 68 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 69 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 70 | endif() 71 | 72 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 73 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") 74 | 75 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 76 | COMPONENT Runtime) 77 | 78 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 79 | COMPONENT Runtime) 80 | 81 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 82 | COMPONENT Runtime) 83 | 84 | if(PLUGIN_BUNDLED_LIBRARIES) 85 | install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" 86 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 87 | COMPONENT Runtime) 88 | endif() 89 | 90 | # Copy the native assets provided by the build.dart from all packages. 91 | set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") 92 | install(DIRECTORY "${NATIVE_ASSETS_DIR}" 93 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 94 | COMPONENT Runtime) 95 | 96 | # Fully re-copy the assets directory on each build to avoid having stale files 97 | # from a previous install. 98 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 99 | install(CODE " 100 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 101 | " COMPONENT Runtime) 102 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 103 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 104 | 105 | # Install the AOT library on non-Debug builds only. 106 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 107 | CONFIGURATIONS Profile;Release 108 | COMPONENT Runtime) 109 | -------------------------------------------------------------------------------- /example/demo_app/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: demo_app 2 | description: "A demo app with flutter that is bundled into exe installer using inno_bundle package." 3 | # The following line prevents the package from being accidentally published to 4 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 5 | publish_to: "none" # Remove this line if you wish to publish to pub.dev 6 | 7 | # The following defines the version and build number for your application. 8 | # A version number is three numbers separated by dots, like 1.2.43 9 | # followed by an optional build number separated by a +. 10 | # Both the version and the builder number may be overridden in flutter 11 | # build by specifying --build-name and --build-number, respectively. 12 | # In Android, build-name is used as versionName while build-number used as versionCode. 13 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 14 | # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. 15 | # Read more about iOS versioning at 16 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 17 | # In Windows, build-name is used as the major, minor, and patch parts 18 | # of the product and file versions while build-number is used as the build suffix. 19 | version: 1.0.0+1 20 | 21 | environment: 22 | sdk: ">=3.2.3 <4.0.0" 23 | 24 | # Dependencies specify other packages that your package needs in order to work. 25 | # To automatically upgrade your package dependencies to the latest versions 26 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 27 | # dependencies can be manually updated by changing the version numbers below to 28 | # the latest version available on pub.dev. To see which dependencies have newer 29 | # versions available, run `flutter pub outdated`. 30 | dependencies: 31 | flutter: 32 | sdk: flutter 33 | 34 | dev_dependencies: 35 | flutter_test: 36 | sdk: flutter 37 | inno_bundle: 38 | path: ../.. 39 | 40 | # The "flutter_lints" package below contains a set of recommended lints to 41 | # encourage good coding practices. The lint set provided by the package is 42 | # activated in the `analysis_options.yaml` file located at the root of your 43 | # package. See that file for information about deactivating specific lint 44 | # rules and activating additional ones. 45 | flutter_lints: ^6.0.0 46 | 47 | # For information on the generic Dart part of this file, see the 48 | # following page: https://dart.dev/tools/pub/pubspec 49 | 50 | # The following section is specific to Flutter packages. 51 | flutter: 52 | # The following line ensures that the Material Icons font is 53 | # included with your application, so that you can use the icons in 54 | # the material Icons class. 55 | uses-material-design: true 56 | 57 | # To add assets to your application, add an assets section, like this: 58 | # assets: 59 | # - images/a_dot_burr.jpeg 60 | # - images/a_dot_ham.jpeg 61 | 62 | # An image asset can refer to one or more resolution-specific "variants", see 63 | # https://flutter.dev/assets-and-images/#resolution-aware 64 | 65 | # For details regarding adding assets from package dependencies, see 66 | # https://flutter.dev/assets-and-images/#from-packages 67 | 68 | # To add custom fonts to your application, add a fonts section here, 69 | # in this "flutter" section. Each entry in this list should have a 70 | # "family" key with the font family name, and a "fonts" key with a 71 | # list giving the asset and other descriptors for the font. For 72 | # example: 73 | # fonts: 74 | # - family: Schyler 75 | # fonts: 76 | # - asset: fonts/Schyler-Regular.ttf 77 | # - asset: fonts/Schyler-Italic.ttf 78 | # style: italic 79 | # - family: Trajan Pro 80 | # fonts: 81 | # - asset: fonts/TrajanPro.ttf 82 | # - asset: fonts/TrajanPro_Bold.ttf 83 | # weight: 700 84 | # 85 | # For details regarding fonts from package dependencies, 86 | # see https://flutter.dev/custom-fonts/#from-packages 87 | 88 | inno_bundle: 89 | id: 5ec949d0-0582-1e06-b073-b5d1161f6fff 90 | publisher: Hocine Abdellatif Houari 91 | installer_icon: assets/images/installer.ico 92 | license_file: assets/LICENSE.txt 93 | languages: 94 | - english 95 | - french 96 | - arabic 97 | admin: auto 98 | arch: x64_compatible 99 | files: 100 | - path: msvcp140.dll 101 | source: system32 102 | destination: lalaland 103 | name: msvcp140lalaland.dll 104 | # vc_redist: download 105 | # sign_tool: 106 | # command: $p 107 | -------------------------------------------------------------------------------- /lib/models/sign_tool.dart: -------------------------------------------------------------------------------- 1 | /// A class that holds the properties related to the signing tool used by Inno Setup, 2 | /// based on the directives in the Setup section. 3 | /// 4 | /// The [SignTool] class encapsulates details such as the tool's name, the command 5 | /// used to invoke it, additional parameters, and retry configurations. It also provides 6 | /// methods for validating and parsing configuration options to create an instance of [SignTool]. 7 | /// 8 | /// Example usage: 9 | /// ```dart 10 | /// var signTool = SignTool( 11 | /// name: "MySignTool", 12 | /// command: "signtool.exe $p", 13 | /// params: "/a /tr http://timestamp.url", 14 | /// retryCount: 2, 15 | /// retryDelay: 200, 16 | /// ); 17 | /// 18 | /// print(signTool.inno); 19 | /// // Outputs: 20 | /// // SignTool=MySignTool /a /tr http://timestamp.url 21 | /// // SignToolRetryCount=2 22 | /// // SignToolRetryDelay=200 23 | /// ``` 24 | /// 25 | /// Properties: 26 | /// - [name]: The name of the signing tool. 27 | /// - [command]: The command used to invoke the signing tool. 28 | /// - [params]: Additional parameters passed to the signing tool. 29 | /// - [retryCount]: The number of times to retry the signing operation on failure. 30 | /// - [retryDelay]: The delay in milliseconds between retry attempts. 31 | /// 32 | /// Methods: 33 | /// - [validateConfig]: Validates the configuration option for [SignTool]. Returns a string describing the error, or `null` if valid. 34 | /// - [fromOption]: Parses the configuration option into a [SignTool] instance. Accepts both string and map formats. 35 | /// - [inno]: Generates the Inno Setup script directives for the signing tool configuration. 36 | library; 37 | 38 | /// Class holding the sign tool properties based on Inno Setup approach and Setup section directives 39 | class SignTool { 40 | /// The name of the signing tool. 41 | final String name; 42 | 43 | /// The command used to invoke the signing tool. 44 | final String command; 45 | 46 | /// Additional parameters passed to the signing tool. 47 | final String params; 48 | 49 | /// The number of times to retry the signing operation on failure. 50 | final int retryCount; 51 | 52 | /// The delay in milliseconds between retry attempts. 53 | final int retryDelay; 54 | 55 | /// Create a [SignTool] instance with the given properties. 56 | const SignTool({ 57 | required this.name, 58 | required this.command, 59 | required this.params, 60 | required this.retryCount, 61 | required this.retryDelay, 62 | }); 63 | 64 | /// Validate configuration option for [SignTool] 65 | static String? validateConfig( 66 | dynamic option, { 67 | required String configName, 68 | String? signToolName, 69 | String? signToolCommand, 70 | String? signToolParams, 71 | }) { 72 | if (option == null || option is String) return null; 73 | if (option is Map) { 74 | if ((signToolName ?? option['name']) == null && 75 | (signToolCommand ?? option['command']) == null) { 76 | return "inno_bundle.sign_tool in $configName is expected to be " 77 | "of type String or to at least have name or command fields."; 78 | } 79 | return null; 80 | } 81 | 82 | return "inno_bundle.sign_tool attribute is invalid in $configName."; 83 | } 84 | 85 | /// Parses configuration option to the desired [SignTool]. 86 | static SignTool? fromOption( 87 | dynamic option, { 88 | String? signToolName, 89 | String? signToolCommand, 90 | String? signToolParams, 91 | }) { 92 | if (option == null) { 93 | if (signToolName != null || 94 | signToolCommand != null || 95 | signToolParams != null) { 96 | return SignTool( 97 | name: signToolName ?? "InnoBundleTool", 98 | command: signToolCommand ?? "", 99 | params: signToolParams ?? "", 100 | retryCount: 2, 101 | retryDelay: 500, 102 | ); 103 | } else { 104 | return null; 105 | } 106 | } 107 | if (option is String) { 108 | return SignTool( 109 | name: signToolName ?? "InnoBundleTool", 110 | command: signToolCommand ?? option, 111 | params: signToolParams ?? "", 112 | retryCount: 2, 113 | retryDelay: 500, 114 | ); 115 | } 116 | final map = option as Map; 117 | return SignTool( 118 | name: signToolName ?? map['name'] ?? "InnoBundleTool", 119 | command: signToolCommand ?? map["command"] ?? "", 120 | params: signToolParams ?? map["params"] ?? "", 121 | retryCount: map["retry_count"] ?? 2, 122 | retryDelay: map["retry_delay"] ?? 500, 123 | ); 124 | } 125 | 126 | /// Get inno setup code based on this [SignTool] values. 127 | String toInnoCode() { 128 | return """ 129 | SignTool=$name $params 130 | SignToolRetryCount=$retryCount 131 | SignToolRetryDelay=$retryDelay 132 | """; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /example/demo_app/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | void main() { 4 | runApp(const MyApp()); 5 | } 6 | 7 | class MyApp extends StatelessWidget { 8 | const MyApp({super.key}); 9 | 10 | // This widget is the root of your application. 11 | @override 12 | Widget build(BuildContext context) { 13 | return MaterialApp( 14 | title: 'Flutter Demo', 15 | theme: ThemeData( 16 | // This is the theme of your application. 17 | // 18 | // TRY THIS: Try running your application with "flutter run". You'll see 19 | // the application has a purple toolbar. Then, without quitting the app, 20 | // try changing the seedColor in the colorScheme below to Colors.green 21 | // and then invoke "hot reload" (save your changes or press the "hot 22 | // reload" button in a Flutter-supported IDE, or press "r" if you used 23 | // the command line to start the app). 24 | // 25 | // Notice that the counter didn't reset back to zero; the application 26 | // state is not lost during the reload. To reset the state, use hot 27 | // restart instead. 28 | // 29 | // This works for code too, not just values: Most code changes can be 30 | // tested with just a hot reload. 31 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), 32 | useMaterial3: true, 33 | ), 34 | home: const MyHomePage(title: 'Flutter Demo Home Page'), 35 | ); 36 | } 37 | } 38 | 39 | class MyHomePage extends StatefulWidget { 40 | const MyHomePage({super.key, required this.title}); 41 | 42 | // This widget is the home page of your application. It is stateful, meaning 43 | // that it has a State object (defined below) that contains fields that affect 44 | // how it looks. 45 | 46 | // This class is the configuration for the state. It holds the values (in this 47 | // case the title) provided by the parent (in this case the App widget) and 48 | // used by the build method of the State. Fields in a Widget subclass are 49 | // always marked "final". 50 | 51 | final String title; 52 | 53 | @override 54 | State createState() => _MyHomePageState(); 55 | } 56 | 57 | class _MyHomePageState extends State { 58 | int _counter = 0; 59 | 60 | void _incrementCounter() { 61 | setState(() { 62 | // This call to setState tells the Flutter framework that something has 63 | // changed in this State, which causes it to rerun the build method below 64 | // so that the display can reflect the updated values. If we changed 65 | // _counter without calling setState(), then the build method would not be 66 | // called again, and so nothing would appear to happen. 67 | _counter++; 68 | }); 69 | } 70 | 71 | @override 72 | Widget build(BuildContext context) { 73 | // This method is rerun every time setState is called, for instance as done 74 | // by the _incrementCounter method above. 75 | // 76 | // The Flutter framework has been optimized to make rerunning build methods 77 | // fast, so that you can just rebuild anything that needs updating rather 78 | // than having to individually change instances of widgets. 79 | return Scaffold( 80 | appBar: AppBar( 81 | // TRY THIS: Try changing the color here to a specific color (to 82 | // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar 83 | // change color while the other colors stay the same. 84 | backgroundColor: Theme.of(context).colorScheme.inversePrimary, 85 | // Here we take the value from the MyHomePage object that was created by 86 | // the App.build method, and use it to set our appbar title. 87 | title: Text(widget.title), 88 | ), 89 | body: Center( 90 | // Center is a layout widget. It takes a single child and positions it 91 | // in the middle of the parent. 92 | child: Column( 93 | // Column is also a layout widget. It takes a list of children and 94 | // arranges them vertically. By default, it sizes itself to fit its 95 | // children horizontally, and tries to be as tall as its parent. 96 | // 97 | // Column has various properties to control how it sizes itself and 98 | // how it positions its children. Here we use mainAxisAlignment to 99 | // center the children vertically; the main axis here is the vertical 100 | // axis because Columns are vertical (the cross axis would be 101 | // horizontal). 102 | // 103 | // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" 104 | // action in the IDE, or press "p" in the console), to see the 105 | // wireframe for each widget. 106 | mainAxisAlignment: MainAxisAlignment.center, 107 | children: [ 108 | const Text( 109 | 'You have pushed the button this many times:', 110 | ), 111 | Text( 112 | '$_counter', 113 | style: Theme.of(context).textTheme.headlineMedium, 114 | ), 115 | ], 116 | ), 117 | ), 118 | floatingActionButton: FloatingActionButton( 119 | onPressed: _incrementCounter, 120 | tooltip: 'Increment', 121 | child: const Icon(Icons.add), 122 | ), // This trailing comma makes auto-formatting nicer for build methods. 123 | ); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /bin/inno_bundle.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:args/args.dart'; 4 | import 'package:inno_bundle/builders/app_builder.dart'; 5 | import 'package:inno_bundle/builders/installer_builder.dart'; 6 | import 'package:inno_bundle/builders/script_builder.dart'; 7 | import 'package:inno_bundle/models/build_type.dart'; 8 | import 'package:inno_bundle/models/cli_config.dart'; 9 | import 'package:inno_bundle/models/config.dart'; 10 | import 'package:inno_bundle/utils/cli_logger.dart'; 11 | import 'package:inno_bundle/utils/constants.dart'; 12 | import 'package:inno_bundle/utils/functions.dart'; 13 | 14 | /// Builds the application using the provided configuration. 15 | /// 16 | /// Returns the directory containing the built application files. 17 | Future _buildApp(Config config) async { 18 | final builder = AppBuilder(config); 19 | return await builder.build(); 20 | } 21 | 22 | /// Generates the Inno Setup script file for the installer. 23 | /// 24 | /// Returns the generated Inno Setup script file. 25 | Future _buildScript(Config config, Directory appDir) async { 26 | final builder = ScriptBuilder(config, appDir); 27 | return await builder.build(); 28 | } 29 | 30 | /// Builds the installer using the provided configuration and Inno Setup script file. 31 | Future _buildInstaller(Config config, File scriptFile) async { 32 | final builder = InstallerBuilder(config, scriptFile); 33 | await builder.build(); 34 | } 35 | 36 | /// Run to build installer 37 | void main(List arguments) async { 38 | final parser = ArgParser() 39 | ..addFlag(BuildType.release.name, negatable: false, help: 'Default flag') 40 | ..addFlag(BuildType.profile.name, negatable: false) 41 | ..addFlag(BuildType.debug.name, negatable: false) 42 | ..addFlag('app', defaultsTo: true, help: 'Build app') 43 | ..addFlag('installer', defaultsTo: true, help: 'Build installer') 44 | ..addFlag( 45 | 'install-inno', 46 | defaultsTo: true, 47 | help: 'Install Inno Setup into your system if not already installed\n' 48 | 'This requires Winget to be already available on the system', 49 | ) 50 | ..addFlag( 51 | 'gen-app-id', 52 | defaultsTo: true, 53 | help: 'Generate a random App ID into your config file if non-existent\n' 54 | 'This will use namespace from --app-id-ns if provided', 55 | ) 56 | ..addOption( 57 | 'path', 58 | help: 'Path to custom config file. Default: pubspec.yaml', 59 | ) 60 | ..addOption( 61 | "app-id-ns", 62 | help: "Namespace for --gen-app-id\nExample: www.example.com", 63 | ) 64 | ..addFlag( 65 | 'gen-publisher', 66 | defaultsTo: true, 67 | help: 'Generate a publisher name into config file if non-existent\n' 68 | 'This will generate based on username of logged in user in machine\n' 69 | 'and only if maintainer field is not present in config file', 70 | ) 71 | ..addOption("build-args", help: "Append args to \"flutter build ...\"") 72 | ..addOption("shorebird-args", 73 | help: "Append args to \"shorebird release windows ...\"") 74 | ..addOption( 75 | "build-tool", 76 | help: "Select build tool: flutter (default) or shorebird", 77 | allowed: ["flutter", "shorebird"], 78 | defaultsTo: "flutter", 79 | ) 80 | ..addOption("app-version", help: "Override app version") 81 | ..addOption("sign-tool-name", help: "Override sign tool name") 82 | ..addOption("sign-tool-command", help: "Override sign tool command") 83 | ..addOption("sign-tool-params", help: "Override sign tool params") 84 | ..addFlag( 85 | 'envs', 86 | defaultsTo: false, 87 | negatable: false, 88 | help: "Print env variables and exit", 89 | ) 90 | ..addFlag('hf', defaultsTo: true, help: 'Print header and footer') 91 | ..addFlag('help', abbr: 'h', negatable: false, help: 'Print help and exit'); 92 | final parsedArgs = parser.parse(arguments); 93 | final envs = parsedArgs['envs'] as bool; 94 | final hf = parsedArgs['hf'] as bool; 95 | final help = parsedArgs['help'] as bool; 96 | 97 | if (hf) print(START_MESSAGE); 98 | 99 | if (help) { 100 | print("${parser.usage}\n"); 101 | exit(0); 102 | } 103 | 104 | final cliConfig = CliConfig.fromArgs(parsedArgs); 105 | const pubspecFilePath = 'pubspec.yaml'; 106 | final pubspecFile = File(pubspecFilePath); 107 | final defaultConfigFilePath = 'inno_bundle.yaml'; 108 | final defaultConfigFile = File(defaultConfigFilePath); 109 | final configFilePath = parsedArgs['path'] as String?; 110 | 111 | // if config file points to pubspec file, use same File instance, 112 | // the intention is to first look up custom config file, 113 | // if not provided, look up default config file `inno_bundle.yaml`, 114 | // else, then look up pubspec file. 115 | final configFile = configFilePath == pubspecFilePath 116 | ? pubspecFile 117 | : configFilePath != null 118 | ? File(configFilePath) 119 | : defaultConfigFile.existsSync() 120 | ? defaultConfigFile 121 | : pubspecFile; 122 | 123 | if (cliConfig.generateAppId || cliConfig.generatePublisher) { 124 | generateEssentials(pubspecFile, configFile, cliConfig); 125 | } 126 | 127 | final config = Config.fromFile(pubspecFile, configFile, cliConfig); 128 | 129 | if (envs) { 130 | print(config.toEnvironmentVariables()); 131 | exit(0); 132 | } 133 | 134 | if (cliConfig.installInnoSetup && cliConfig.installer) { 135 | await installInnoSetup(); 136 | } 137 | 138 | final appBuildDir = await _buildApp(config); 139 | final scriptFile = await _buildScript(config, appBuildDir); 140 | await _buildInstaller(config, scriptFile); 141 | CliLogger.flushDeferred(); 142 | 143 | if (hf) print(BUILD_END_MESSAGE); 144 | } 145 | -------------------------------------------------------------------------------- /lib/models/file_entry.dart: -------------------------------------------------------------------------------- 1 | /// A class that represents a file entry for use in Inno Setup. 2 | /// 3 | /// The [FileEntry] class encapsulates the path to the file, an optional name, 4 | /// a flag indicating if the file is required, and the source of the file 5 | /// (either the project or the system32 directory). 6 | /// 7 | /// Example usage: 8 | /// ```dart 9 | /// var fileEntry = FileEntry( 10 | /// path: "mydll.dll", 11 | /// required: true, 12 | /// source: FileSource.project, 13 | /// ); 14 | /// 15 | /// print(fileEntry.path); // Outputs: mydll.dll 16 | /// print(fileEntry.name); // Outputs: mydll.dll 17 | /// print(fileEntry.required); // Outputs: true 18 | /// print(fileEntry.source); // Outputs: FileSource.project 19 | /// ``` 20 | /// 21 | /// Properties: 22 | /// - [path]: The path to the file. 23 | /// - [name]: The name for the file. Defaults to the basename of the path. 24 | /// - [required]: A flag indicating if the file is required. Defaults to true. 25 | /// - [source]: An enum indicating the source of the file. Defaults to FileSource.project. 26 | /// - [destination]: The destination directory for the file. 27 | library; 28 | 29 | import 'dart:io'; 30 | 31 | import 'package:inno_bundle/utils/constants.dart'; 32 | import 'package:path/path.dart' as p; 33 | 34 | /// Enum representing the source of the extra files to be included with the app bundle. 35 | enum FileSource { 36 | /// The files is located in the project directory. 37 | project, 38 | 39 | /// The files is located in the system32 directory. 40 | system32; 41 | 42 | /// Return string array of literal values. 43 | static List get literalValues => values.map((e) => e.name).toList(); 44 | } 45 | 46 | class FileEntry { 47 | /// The path to the file. 48 | final String path; 49 | 50 | /// The name for the file. Defaults to the basename of the path. 51 | final String name; 52 | 53 | /// A flag indicating if the file is required. Defaults to true. 54 | final bool required; 55 | 56 | /// An enum indicating the source of the file. Defaults to FileSource.project. 57 | final FileSource source; 58 | 59 | /// Destination directory for the file. 60 | final String? destination; 61 | 62 | /// Create a [FileEntry] instance with the given properties. 63 | const FileEntry({ 64 | required this.path, 65 | required this.name, 66 | this.required = true, 67 | this.source = FileSource.project, 68 | this.destination, 69 | }); 70 | 71 | /// Validate configuration option for [FileEntry]. 72 | static String? validateConfig(dynamic option, {required String configName}) { 73 | if (option == null) return null; 74 | if (option is String) { 75 | return null; 76 | } 77 | if (option is Map) { 78 | if (option['path'] == null) { 79 | return "path field is missing from inno_bundle.files entry in $configName, " 80 | "it must be a string."; 81 | } 82 | final path = option['path']; 83 | if (path is! String) { 84 | return "path field in inno_bundle.files entry in $configName must be a string."; 85 | } 86 | if (option['name'] != null && option['name'] is! String) { 87 | return "name field in inno_bundle.files entry in $configName must be a string or null."; 88 | } 89 | if (option['required'] != null && option['required'] is! bool) { 90 | return "required field in inno_bundle.files entry in $configName must be a boolean or null."; 91 | } 92 | if (option['source'] != null && 93 | (option['source'] is! String || 94 | !FileSource.literalValues.contains(option['source']))) { 95 | return "source field in inno_bundle.files entry in $configName must be " 96 | "one of ${FileSource.literalValues.join(', ')} or null."; 97 | } 98 | return null; 99 | } 100 | 101 | return "inno_bundle.files attribute is invalid in $configName."; 102 | } 103 | 104 | /// Parse [FileSource] from string or null, default to [FileSource.project] 105 | static FileSource _parseSource(dynamic source, String path) { 106 | // if source is null, we assume project as a base to the path 107 | if (source == null) return FileSource.project; 108 | if (source is! String) throw ArgumentError('Invalid FileSource: $source'); 109 | switch (source.toLowerCase()) { 110 | case 'project': 111 | return FileSource.project; 112 | case 'system32': 113 | return FileSource.system32; 114 | default: 115 | throw ArgumentError('Invalid FileSource: $source'); 116 | } 117 | } 118 | 119 | /// Creates a [FileEntry] instance from a JSON map (or String) 120 | factory FileEntry.fromJson(dynamic json) { 121 | assert(json != null); 122 | if (json is String) { 123 | return FileEntry( 124 | path: json, 125 | name: p.basename(json), 126 | required: true, 127 | source: FileSource.project, 128 | destination: null, 129 | ); 130 | } 131 | final path = json['path'] as String; 132 | final name = json['name'] as String? ?? p.basename(path); 133 | final required = json['required'] as bool? ?? true; 134 | final source = _parseSource(json['source'], path); 135 | final destination = json['destination'] as String?; 136 | return FileEntry( 137 | path: path, 138 | name: name, 139 | required: required, 140 | source: source, 141 | destination: destination, 142 | ); 143 | } 144 | 145 | /// Get file absolute path. 146 | String get absolutePath { 147 | if (p.isAbsolute(path)) return path; 148 | if (source == FileSource.project) { 149 | return p.join(Directory.current.path, path); 150 | } 151 | if (source == FileSource.system32) return p.joinAll([...system32, path]); 152 | // this package is not supposed to arrive to this line, but just in case. 153 | throw ArgumentError('Invalid FileSource: $source'); 154 | } 155 | 156 | /// Get dll files entries for vc redistributable DLLs. 157 | static List get vcEntries => vcDllFiles 158 | .map((file) => FileEntry( 159 | path: file, 160 | name: p.basename(file), 161 | required: false, 162 | source: FileSource.system32, 163 | )) 164 | .toList(growable: false); 165 | } 166 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.11.2 2 | 3 | - Add screenshot to `pubspec.yaml`, it is used to display the package icon in [pub.dev](https://pub.dev/). 4 | 5 | ## 0.11.1 6 | 7 | - Update `README.md` to include topics and links to wiki pages. 8 | 9 | ## 0.11.0 10 | 11 | - **`(Deprecation!)`** Deprecate `dlls` attribute in `inno_bundle` section, use `files` instead. 12 | 13 | - **`(Deprecation!)`** Deprecate `dart run inno_bundle:build` command, use `dart run inno_bundle` instead, `build` is now the default command. 14 | 15 | - Add `files` attribute to `inno_bundle` section to allow including additional files in the installer [#18](https://github.com/hahouari/inno_bundle/pull/18). 16 | 17 | - Add `destination` attribute to `FileEntry` to specify the destination directory for the file. 18 | 19 | - Fix VC++ redistributable checkbox position when installer window is resizing. 20 | 21 | - Enhance error messages by implementing `DeferredMessage` class to print important messages at the end of the installer generation process. 22 | 23 | - Clean flutter dependency entirely from the package. 24 | 25 | ## 0.10.1 26 | 27 | - Update `README.md` to align with breaking changes in `0.10.0`. 28 | 29 | - Correct position of checkbox that downloads VC++ redistributable in last page of the installer. 30 | 31 | ## 0.10.0 32 | 33 | - **`(Breaking!)`** Change the default build type from `debug` to `release`, this was changed to match the expected default behavior of an app bundler. 34 | 35 | - Adds `--path` CLI parameter to specify custom config file path [#15](https://github.com/hahouari/inno_bundle/pull/15#issuecomment-3268840258), also support reading config from `inno_bundle.yaml` file if available. 36 | 37 | - Add environment variable `CONFIG_FILE` to CLI output of `dart run inno_bundle:build --envs`. 38 | 39 | - Update help and error messages to reflect which config file is being used. 40 | 41 | - Change unresolved publisher name from `Unknown User` to `Unknown Publisher`. 42 | 43 | - Update Inno Setup installation steps link. 44 | 45 | - Update Flutter Icon to reduce its bundle size from ~32KB to ~3.76KB. 46 | 47 | ## 0.9.0 48 | 49 | - Adds config to control VC++ redistributable, either to bundle it with the installer or download it during installation [#13](https://github.com/hahouari/inno_bundle/issues/13). 50 | 51 | - Adds support for including DLL files [#11](https://github.com/hahouari/inno_bundle/issues/11). 52 | 53 | ## 0.8.0 54 | 55 | - **`(Breaking!)`** Move to Inno Setup version `6.4.1`. 56 | 57 | - Add installing Inno Setup using `--install-inno` CLI flag. 58 | 59 | - Add generating App ID using `--gen-app-id` CLI flag. 60 | 61 | - Add `--app-id-ns` CLI flag to generate App ID with namespace. 62 | 63 | - Add generating publisher name using `--gen-publisher` CLI flag. 64 | 65 | - Add `arabic`, `swedish`, and `tamil` languages support. 66 | 67 | - Update Flutter SDK minimum version to `3.16.0`. 68 | 69 | - Update some packages versions. 70 | 71 | - Update `README.md` for fewer steps in Guide section. 72 | 73 | - Remove overriding the default exe file name. 74 | 75 | ## 0.7.4 76 | 77 | - Update `uuid` package and replace deprecated `Uuid.NAMESPACE_URL` with `Namespace.url`. 78 | 79 | - Reverse `CHANGELOG.md` order. 80 | 81 | - Specify platform in `pubspec.yaml` as `windows` only. 82 | 83 | - Update `README.md` summary. 84 | 85 | ## 0.7.3 86 | 87 | - More docs for 100% score on pub.dev documentation. 88 | 89 | ## 0.7.2 90 | 91 | - More docs, trying to top up the [package score](https://pub.dev/packages/inno_bundle/score). 92 | 93 | ## 0.7.1 94 | 95 | - Add `korean` language support. 96 | 97 | - Improve file-level documentation. 98 | 99 | ## 0.7.0 100 | 101 | - **`(Breaking!)`** Move to Inno Setup version `6.3.3`. 102 | 103 | - Add `auto` value to `admin` attribute. 104 | 105 | - Add `arch` attribute to enable or disable x64 emulation on Arm devices. 106 | 107 | - Rewrite `README.md` into much smaller version. 108 | 109 | - Move configuration details and examples to [GitHub Wiki](https://github.com/hahouari/inno_bundle/wiki). 110 | 111 | - Add `sign_tool` field to sign installer and uninstaller [#7](https://github.com/hahouari/inno_bundle/pull/7). 112 | 113 | - Add `--sign-tool-name`, `--sign-tool-command`, `--sign-tool-params` CLI parameters to partially override `sign_tool` attribute. 114 | 115 | - Trivial update to packages versions. 116 | 117 | ## 0.6.1 118 | 119 | - Fix `admin` attribute was not resolving correctly from `pubspec.yaml` [#4](https://github.com/hahouari/inno_bundle/pull/4). 120 | 121 | - Add `license_file` attribute to `inno_bundle` section to accommodate software license file [#5](https://github.com/hahouari/inno_bundle/pull/5). 122 | 123 | - Look up project root folder for `LICENSE` file if `license_file` attribute is not provided. 124 | 125 | - Change default checked to `true` for add desktop icon checkbox. 126 | 127 | - Optimize published library size by excluding installer ico and svg files. 128 | 129 | ## 0.6.0 130 | 131 | - Add `--app-version` argument to `build` command to override app version from CLI. 132 | 133 | - Add `--build-args` argument to `build` command to append more args into `flutter build`. 134 | 135 | - Improve building app to include obfuscation during app build. 136 | 137 | - Improve uninstaller UI/UX info. 138 | 139 | - Add installer SVG file to demo assets under MIT license. (not a feature, but worth tracking) 140 | 141 | - Add Inno Setup installation step through `Chocolatey`. 142 | 143 | - Improve error messages and suggest repo link as guide for corrupted installs of Inno Setup. 144 | 145 | ## 0.5.0 146 | 147 | - Update packages and lower back minimum dart and flutter versions. 148 | 149 | - Rework usage of app name and pubspec name props. See [#2](https://github.com/hahouari/inno_bundle/issues/2). 150 | 151 | ## 0.4.2 152 | 153 | - Update packages version and minimum dart and flutter version to latest. 154 | 155 | ## 0.4.1 156 | 157 | - Add guide to setup GitHub Workflows and automate installer build as GitHub releases. 158 | 159 | ## 0.4.0 160 | 161 | - Add `--hf` and `--no-hf` flags to control printing of header and footer text. 162 | 163 | - Add `--envs` to `build` command to print resolved config as environment variables and exit. 164 | 165 | - Add `--help` to `id` command. 166 | 167 | ## 0.3.3 168 | 169 | - Fix issue icon not persisting in the system temp directory when using default installer icon. 170 | 171 | - Update iss script maintainer clause. 172 | 173 | - Rename generated iss script to `inno-script.iss`. 174 | 175 | ## 0.3.2 176 | 177 | - Add documentation to codebase. 178 | 179 | ## 0.3.1 180 | 181 | - Update README.md for winget installation option of Inno Setup. 182 | 183 | - Replace LinkedIn link in maintainer clause for generated `iss script` with GitHub link. 184 | 185 | ## 0.3.0 186 | 187 | - Replace `--skip-app` with `--app` and `--no-app` flags, default to `--app`. 188 | 189 | - Add `--installer` and `--no-installer` flags, default to `--installer`. 190 | 191 | - Add `--help` and `-h` with descriptive messages to each flag. 192 | 193 | - Refactor `Config` to include CLI arguments as well. 194 | 195 | - Make `.iss script` generation happen under `build\windows\x64\installer\\.iss`. 196 | 197 | - Add `%UserProfile%\Local\Programs\Inno Setup 6` as a possible path to find Inno Setup installation. 198 | 199 | - Update error message for option to install Inno Setup using `winget` when not detected on the machine. 200 | 201 | ## 0.2.0 202 | 203 | - Update default icon, old one is not permissible to use for commercial use without license, so I created new one from **license free** resources. 204 | 205 | - Clean cupertino icons from example as it is unused. 206 | 207 | - Clean `lib\inno_bundle.dart` and `test\inno_bundle_test.dart`. 208 | 209 | - When default icon is used, the new one is copied to `%TEMP%` folder on every installer build. I did not find an efficient way to update the old one. 210 | 211 | ## 0.1.1 212 | 213 | - Update README.md. 214 | 215 | ## 0.1.0 216 | 217 | - Make `installer_icon` optional attribute. 218 | 219 | ## 0.0.3 220 | 221 | - Fix `languages` attribute name in README.md example. 222 | 223 | ## 0.0.2 224 | 225 | - Add a README.md to example and update main README.md. 226 | 227 | ## 0.0.1 228 | 229 | - Add a working bundler and example. 230 | -------------------------------------------------------------------------------- /example/demo_app/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | args: 5 | dependency: transitive 6 | description: 7 | name: args 8 | sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "2.7.0" 12 | async: 13 | dependency: transitive 14 | description: 15 | name: async 16 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.11.0" 20 | boolean_selector: 21 | dependency: transitive 22 | description: 23 | name: boolean_selector 24 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "2.1.1" 28 | characters: 29 | dependency: transitive 30 | description: 31 | name: characters 32 | sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "1.4.0" 36 | clock: 37 | dependency: transitive 38 | description: 39 | name: clock 40 | sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.1.2" 44 | collection: 45 | dependency: transitive 46 | description: 47 | name: collection 48 | sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.19.1" 52 | crypto: 53 | dependency: transitive 54 | description: 55 | name: crypto 56 | sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "3.0.6" 60 | fake_async: 61 | dependency: transitive 62 | description: 63 | name: fake_async 64 | sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "1.3.3" 68 | fixnum: 69 | dependency: transitive 70 | description: 71 | name: fixnum 72 | sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "1.1.1" 76 | flutter: 77 | dependency: "direct main" 78 | description: flutter 79 | source: sdk 80 | version: "0.0.0" 81 | flutter_lints: 82 | dependency: "direct dev" 83 | description: 84 | name: flutter_lints 85 | sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" 86 | url: "https://pub.dev" 87 | source: hosted 88 | version: "6.0.0" 89 | flutter_test: 90 | dependency: "direct dev" 91 | description: flutter 92 | source: sdk 93 | version: "0.0.0" 94 | inno_bundle: 95 | dependency: "direct dev" 96 | description: 97 | path: "../.." 98 | relative: true 99 | source: path 100 | version: "0.11.2" 101 | leak_tracker: 102 | dependency: transitive 103 | description: 104 | name: leak_tracker 105 | sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0" 106 | url: "https://pub.dev" 107 | source: hosted 108 | version: "11.0.1" 109 | leak_tracker_flutter_testing: 110 | dependency: transitive 111 | description: 112 | name: leak_tracker_flutter_testing 113 | sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" 114 | url: "https://pub.dev" 115 | source: hosted 116 | version: "3.0.10" 117 | leak_tracker_testing: 118 | dependency: transitive 119 | description: 120 | name: leak_tracker_testing 121 | sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" 122 | url: "https://pub.dev" 123 | source: hosted 124 | version: "3.0.2" 125 | lints: 126 | dependency: transitive 127 | description: 128 | name: lints 129 | sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 130 | url: "https://pub.dev" 131 | source: hosted 132 | version: "6.0.0" 133 | matcher: 134 | dependency: transitive 135 | description: 136 | name: matcher 137 | sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 138 | url: "https://pub.dev" 139 | source: hosted 140 | version: "0.12.17" 141 | material_color_utilities: 142 | dependency: transitive 143 | description: 144 | name: material_color_utilities 145 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec 146 | url: "https://pub.dev" 147 | source: hosted 148 | version: "0.11.1" 149 | meta: 150 | dependency: transitive 151 | description: 152 | name: meta 153 | sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c 154 | url: "https://pub.dev" 155 | source: hosted 156 | version: "1.16.0" 157 | path: 158 | dependency: transitive 159 | description: 160 | name: path 161 | sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" 162 | url: "https://pub.dev" 163 | source: hosted 164 | version: "1.9.1" 165 | sky_engine: 166 | dependency: transitive 167 | description: flutter 168 | source: sdk 169 | version: "0.0.0" 170 | source_span: 171 | dependency: transitive 172 | description: 173 | name: source_span 174 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 175 | url: "https://pub.dev" 176 | source: hosted 177 | version: "1.10.0" 178 | sprintf: 179 | dependency: transitive 180 | description: 181 | name: sprintf 182 | sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" 183 | url: "https://pub.dev" 184 | source: hosted 185 | version: "7.0.0" 186 | stack_trace: 187 | dependency: transitive 188 | description: 189 | name: stack_trace 190 | sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" 191 | url: "https://pub.dev" 192 | source: hosted 193 | version: "1.12.1" 194 | stream_channel: 195 | dependency: transitive 196 | description: 197 | name: stream_channel 198 | sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" 199 | url: "https://pub.dev" 200 | source: hosted 201 | version: "2.1.4" 202 | string_scanner: 203 | dependency: transitive 204 | description: 205 | name: string_scanner 206 | sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" 207 | url: "https://pub.dev" 208 | source: hosted 209 | version: "1.3.0" 210 | term_glyph: 211 | dependency: transitive 212 | description: 213 | name: term_glyph 214 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 215 | url: "https://pub.dev" 216 | source: hosted 217 | version: "1.2.1" 218 | test_api: 219 | dependency: transitive 220 | description: 221 | name: test_api 222 | sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" 223 | url: "https://pub.dev" 224 | source: hosted 225 | version: "0.7.6" 226 | typed_data: 227 | dependency: transitive 228 | description: 229 | name: typed_data 230 | sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 231 | url: "https://pub.dev" 232 | source: hosted 233 | version: "1.4.0" 234 | uuid: 235 | dependency: transitive 236 | description: 237 | name: uuid 238 | sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff 239 | url: "https://pub.dev" 240 | source: hosted 241 | version: "4.5.1" 242 | vector_math: 243 | dependency: transitive 244 | description: 245 | name: vector_math 246 | sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b 247 | url: "https://pub.dev" 248 | source: hosted 249 | version: "2.2.0" 250 | vm_service: 251 | dependency: transitive 252 | description: 253 | name: vm_service 254 | sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b 255 | url: "https://pub.dev" 256 | source: hosted 257 | version: "14.3.0" 258 | yaml: 259 | dependency: transitive 260 | description: 261 | name: yaml 262 | sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce 263 | url: "https://pub.dev" 264 | source: hosted 265 | version: "3.1.3" 266 | sdks: 267 | dart: ">=3.8.0 <4.0.0" 268 | flutter: ">=3.18.0-18.0.pre.54" 269 | -------------------------------------------------------------------------------- /lib/utils/functions.dart: -------------------------------------------------------------------------------- 1 | /// Utility functions for handling YAML, string manipulation, file operations, and system environment. 2 | library; 3 | 4 | import 'dart:convert'; 5 | import 'dart:io'; 6 | import 'dart:typed_data'; 7 | 8 | import 'package:inno_bundle/models/cli_config.dart'; 9 | import 'package:inno_bundle/utils/cli_logger.dart'; 10 | import 'package:inno_bundle/utils/constants.dart'; 11 | import 'package:inno_bundle/utils/installer_icon.dart'; 12 | import 'package:path/path.dart' as p; 13 | import 'package:uuid/uuid.dart'; 14 | import 'package:yaml/yaml.dart'; 15 | 16 | /// Convert yaml list to list, this prevents some weird behaviors that come with [YamlList] type. 17 | List yamlToList(YamlList yamlList) { 18 | final list = []; 19 | for (final value in yamlList) { 20 | if (value is YamlMap) { 21 | list.add(yamlToMap(value)); 22 | } else if (value is YamlList) { 23 | list.add(yamlToList(value)); 24 | } else { 25 | list.add(value); 26 | } 27 | } 28 | return list; 29 | } 30 | 31 | /// Convert yaml to map, this prevents some weird behaviors that come with [YamlMap] type. 32 | Map yamlToMap(YamlMap yamlMap) { 33 | final map = {}; 34 | for (final entry in yamlMap.entries) { 35 | if (entry.value is YamlList) { 36 | map[entry.key as String] = yamlToList(entry.value as YamlList); 37 | } else if (entry.value is YamlMap) { 38 | map[entry.key as String] = yamlToMap(entry.value as YamlMap); 39 | } else { 40 | map[entry.key as String] = entry.value; 41 | } 42 | } 43 | return map; 44 | } 45 | 46 | /// Converts a string to camelCase. 47 | /// 48 | /// Example: `camelCase("hello-world_out there")` returns "helloWorldOutThere". 49 | String camelCase(String value) { 50 | return value 51 | .split(RegExp(r'[-_]|\s')) 52 | .map((word) => capitalize(word)) 53 | .join(''); 54 | } 55 | 56 | /// Capitalizes the first letter of a string. 57 | /// 58 | /// Example: `capitalize("hello")` returns "Hello". 59 | String capitalize(String value) { 60 | if (value.isEmpty) return ""; 61 | return value[0].toUpperCase() + value.substring(1); 62 | } 63 | 64 | /// Persists the default installer icon to a file in the given directory. 65 | /// 66 | /// Decodes a Base64-encoded icon string and writes it to a file in the 67 | /// system temp directory. 68 | /// 69 | /// Returns the absolute path of the saved icon file. 70 | String persistDefaultInstallerIcon(String dirPath) { 71 | Directory(dirPath).createSync(); 72 | final iconPath = p.join(dirPath, defaultInstallerIconFileName); 73 | final file = File(iconPath); 74 | Uint8List bytes = base64.decode(defaultInstallerIcon); 75 | file.writeAsBytesSync(bytes); 76 | return file.absolute.path; 77 | } 78 | 79 | /// Retrieves the user's home directory path. 80 | /// 81 | /// Uses environment variables to determine the home directory based on the operating system. 82 | String getHomeDir() { 83 | String home = ""; 84 | Map envVars = Platform.environment; 85 | if (Platform.isMacOS || Platform.isLinux) { 86 | home = envVars['HOME'] ?? home; 87 | } else if (Platform.isWindows) { 88 | home = envVars['UserProfile'] ?? home; 89 | } 90 | return home; 91 | } 92 | 93 | /// Reads the [yamlFile] and returns a map of its contents. 94 | /// The [yamlFile] is passed as a [File] object instead of being hardcoded 95 | /// because in the future, we may support reading multiple files for configuration. 96 | Map readYaml(File yamlFile) { 97 | final yamlMap = (loadYaml(yamlFile.readAsStringSync()) ?? YamlMap()) as Map; 98 | // yamlMap has the type YamlMap, which has several unwanted side effects 99 | var json = yamlToMap(yamlMap as YamlMap); 100 | return json; 101 | } 102 | 103 | /// Generates a new app id (as UUID) for the app, if not already present. 104 | /// The new id is persisted in the config file. 105 | /// 106 | /// The [ns] parameter is used to generate a namespaced UUID, if provided. 107 | void generateEssentials( 108 | File pubspecFile, 109 | File configFile, 110 | CliConfig cliConfig, 111 | ) { 112 | // if neither app id nor publisher is to be generated, do nothing 113 | if (!cliConfig.generateAppId && !cliConfig.generatePublisher) return; 114 | 115 | final pubspecJson = readYaml(pubspecFile); 116 | 117 | if (!configFile.existsSync()) { 118 | CliLogger.exitError('The CLI param --path has an invalid value, ' 119 | 'the given path does not exist.'); 120 | } 121 | 122 | // if config file is a custom one, read 'inno_bundle' section from it, 123 | // otherwise, read 'inno_bundle' section from pubspec.yaml. 124 | final configJson = 125 | configFile == pubspecFile ? pubspecJson : readYaml(configFile); 126 | final inno = configJson['inno_bundle'] ?? {}; 127 | 128 | // if inno_bundle essentials are already present, do nothing 129 | if (inno['id'] != null || inno['publisher'] != null) return; 130 | 131 | final lines = configFile.readAsLinesSync(); 132 | var innoInsertLine = lines.indexWhere((l) => l.startsWith("inno_bundle:")); 133 | 134 | // if inno_bundle section is not found, add it at the end of the file 135 | if (innoInsertLine == -1) { 136 | // if the last line is not empty, add an empty line before the new section 137 | if (lines.last.trim().isNotEmpty) lines.add(""); 138 | 139 | lines.add("inno_bundle:"); 140 | innoInsertLine = lines.length - 1; 141 | } 142 | 143 | // this does not check if [id] is type string or if it is valid UUID, 144 | // at this point it is user's responsibility to make sure [id] is valid. 145 | if (cliConfig.generateAppId && inno['id'] == null) { 146 | const uuid = Uuid(); 147 | final appId = cliConfig.appIdNamespace != null 148 | ? uuid.v5(Namespace.url.value, cliConfig.appIdNamespace) 149 | : uuid.v1(); 150 | innoInsertLine += 1; 151 | lines.insert(innoInsertLine, " id: $appId"); 152 | } 153 | 154 | if (cliConfig.generatePublisher && 155 | pubspecJson['maintainer'] == null && 156 | inno['publisher'] == null) { 157 | innoInsertLine += 1; 158 | final publisher = getSystemUserName() ?? "Unknown Publisher"; 159 | lines.insert(innoInsertLine, " publisher: $publisher"); 160 | } 161 | 162 | configFile.writeAsStringSync(lines.join('\n')); 163 | } 164 | 165 | /// Checks if Winget is installed on the system, and returns the path to the executable. 166 | Future getWingetExec() async { 167 | final process = await Process.run('where.exe', ['winget']); 168 | 169 | final exitCode = process.exitCode; 170 | if (exitCode != 0) { 171 | CliLogger.error("Winget is not detected in your machine, " 172 | "Passing --install-inno-setup requires Winget to be installed.\n"); 173 | exit(exitCode); 174 | } 175 | return process.stdout.toString().trim(); 176 | } 177 | 178 | /// Installs Inno Setup into your system if not already installed with Winget. 179 | Future installInnoSetup() async { 180 | final innoSetupExec = getInnoSetupExec(throwIfNotFound: false); 181 | if (innoSetupExec != null) { 182 | CliLogger.info("Inno Setup is already installed."); 183 | return; 184 | } 185 | 186 | final wingetExec = await getWingetExec(); 187 | final wingetFile = File(wingetExec); 188 | 189 | final process = await Process.start( 190 | wingetFile.path, 191 | innoSetupInstallationSubCommand, 192 | runInShell: true, 193 | workingDirectory: Directory.current.path, 194 | mode: ProcessStartMode.inheritStdio, 195 | ); 196 | 197 | final exitCode = await process.exitCode; 198 | if (exitCode != 0) exit(exitCode); 199 | } 200 | 201 | /// Locates the Inno Setup executable file, ensuring its proper installation. 202 | /// 203 | /// Throws a [ProcessException] if Inno Setup is not found or is corrupted. 204 | File? getInnoSetupExec({bool throwIfNotFound = true}) { 205 | if (!Directory(p.joinAll(innoSysDirPath)).existsSync() && 206 | !Directory(p.joinAll(innoUserDirPath)).existsSync()) { 207 | if (throwIfNotFound) { 208 | CliLogger.exitError("Inno Setup is not detected in your machine, " 209 | "checkout our docs on how to correctly install it:\n" 210 | "${CliLogger.sLink(innoDownloadStepLink, level: CliLoggerLevel.two)}"); 211 | } 212 | return null; 213 | } 214 | 215 | final sysExec = p.joinAll([...innoSysDirPath, "ISCC.exe"]); 216 | final sysExecFile = File(sysExec); 217 | final userExec = p.joinAll([...innoUserDirPath, "ISCC.exe"]); 218 | final userExecFile = File(userExec); 219 | 220 | if (sysExecFile.existsSync()) return sysExecFile; 221 | if (userExecFile.existsSync()) return userExecFile; 222 | 223 | if (throwIfNotFound) { 224 | CliLogger.exitError("Inno Setup installation in your machine is corrupted " 225 | "or incomplete, checkout our docs on how to correctly install it:\n" 226 | "${CliLogger.sLink(innoDownloadStepLink, level: CliLoggerLevel.two)}"); 227 | } 228 | return null; 229 | } 230 | 231 | /// Get the logged in username in the machine. 232 | String? getSystemUserName() => 233 | Platform.environment['USER'] ?? // Linux/macOS 234 | Platform.environment['USERNAME']; // Windows 235 | -------------------------------------------------------------------------------- /lib/utils/installer_icon.dart: -------------------------------------------------------------------------------- 1 | /// File specific for the icon file name and a base64 encoded icon 2 | library; 3 | 4 | /// Constant for the default installer icon file name. 5 | const defaultInstallerIconFileName = "installer.ico"; 6 | 7 | /// Base64-encoded string representing the default installer icon. 8 | /// 9 | /// This is used to generate a default icon file if none is provided by the user. 10 | /// Because you cannot pack images with a flutter library, it is packed as base64 format. 11 | /// Decoded onto an .ico file in a temp folder to be used by the .iss script. 12 | const defaultInstallerIcon = 13 | "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"; 14 | -------------------------------------------------------------------------------- /example/demo_app/windows/runner/win32_window.cpp: -------------------------------------------------------------------------------- 1 | #include "win32_window.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "resource.h" 7 | 8 | namespace { 9 | 10 | /// Window attribute that enables dark mode window decorations. 11 | /// 12 | /// Redefined in case the developer's machine has a Windows SDK older than 13 | /// version 10.0.22000.0. 14 | /// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute 15 | #ifndef DWMWA_USE_IMMERSIVE_DARK_MODE 16 | #define DWMWA_USE_IMMERSIVE_DARK_MODE 20 17 | #endif 18 | 19 | constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; 20 | 21 | /// Registry key for app theme preference. 22 | /// 23 | /// A value of 0 indicates apps should use dark mode. A non-zero or missing 24 | /// value indicates apps should use light mode. 25 | constexpr const wchar_t kGetPreferredBrightnessRegKey[] = 26 | L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; 27 | constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; 28 | 29 | // The number of Win32Window objects that currently exist. 30 | static int g_active_window_count = 0; 31 | 32 | using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); 33 | 34 | // Scale helper to convert logical scaler values to physical using passed in 35 | // scale factor 36 | int Scale(int source, double scale_factor) { 37 | return static_cast(source * scale_factor); 38 | } 39 | 40 | // Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. 41 | // This API is only needed for PerMonitor V1 awareness mode. 42 | void EnableFullDpiSupportIfAvailable(HWND hwnd) { 43 | HMODULE user32_module = LoadLibraryA("User32.dll"); 44 | if (!user32_module) { 45 | return; 46 | } 47 | auto enable_non_client_dpi_scaling = 48 | reinterpret_cast( 49 | GetProcAddress(user32_module, "EnableNonClientDpiScaling")); 50 | if (enable_non_client_dpi_scaling != nullptr) { 51 | enable_non_client_dpi_scaling(hwnd); 52 | } 53 | FreeLibrary(user32_module); 54 | } 55 | 56 | } // namespace 57 | 58 | // Manages the Win32Window's window class registration. 59 | class WindowClassRegistrar { 60 | public: 61 | ~WindowClassRegistrar() = default; 62 | 63 | // Returns the singleton registrar instance. 64 | static WindowClassRegistrar* GetInstance() { 65 | if (!instance_) { 66 | instance_ = new WindowClassRegistrar(); 67 | } 68 | return instance_; 69 | } 70 | 71 | // Returns the name of the window class, registering the class if it hasn't 72 | // previously been registered. 73 | const wchar_t* GetWindowClass(); 74 | 75 | // Unregisters the window class. Should only be called if there are no 76 | // instances of the window. 77 | void UnregisterWindowClass(); 78 | 79 | private: 80 | WindowClassRegistrar() = default; 81 | 82 | static WindowClassRegistrar* instance_; 83 | 84 | bool class_registered_ = false; 85 | }; 86 | 87 | WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; 88 | 89 | const wchar_t* WindowClassRegistrar::GetWindowClass() { 90 | if (!class_registered_) { 91 | WNDCLASS window_class{}; 92 | window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); 93 | window_class.lpszClassName = kWindowClassName; 94 | window_class.style = CS_HREDRAW | CS_VREDRAW; 95 | window_class.cbClsExtra = 0; 96 | window_class.cbWndExtra = 0; 97 | window_class.hInstance = GetModuleHandle(nullptr); 98 | window_class.hIcon = 99 | LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); 100 | window_class.hbrBackground = 0; 101 | window_class.lpszMenuName = nullptr; 102 | window_class.lpfnWndProc = Win32Window::WndProc; 103 | RegisterClass(&window_class); 104 | class_registered_ = true; 105 | } 106 | return kWindowClassName; 107 | } 108 | 109 | void WindowClassRegistrar::UnregisterWindowClass() { 110 | UnregisterClass(kWindowClassName, nullptr); 111 | class_registered_ = false; 112 | } 113 | 114 | Win32Window::Win32Window() { 115 | ++g_active_window_count; 116 | } 117 | 118 | Win32Window::~Win32Window() { 119 | --g_active_window_count; 120 | Destroy(); 121 | } 122 | 123 | bool Win32Window::Create(const std::wstring& title, 124 | const Point& origin, 125 | const Size& size) { 126 | Destroy(); 127 | 128 | const wchar_t* window_class = 129 | WindowClassRegistrar::GetInstance()->GetWindowClass(); 130 | 131 | const POINT target_point = {static_cast(origin.x), 132 | static_cast(origin.y)}; 133 | HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); 134 | UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); 135 | double scale_factor = dpi / 96.0; 136 | 137 | HWND window = CreateWindow( 138 | window_class, title.c_str(), WS_OVERLAPPEDWINDOW, 139 | Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), 140 | Scale(size.width, scale_factor), Scale(size.height, scale_factor), 141 | nullptr, nullptr, GetModuleHandle(nullptr), this); 142 | 143 | if (!window) { 144 | return false; 145 | } 146 | 147 | UpdateTheme(window); 148 | 149 | return OnCreate(); 150 | } 151 | 152 | bool Win32Window::Show() { 153 | return ShowWindow(window_handle_, SW_SHOWNORMAL); 154 | } 155 | 156 | // static 157 | LRESULT CALLBACK Win32Window::WndProc(HWND const window, 158 | UINT const message, 159 | WPARAM const wparam, 160 | LPARAM const lparam) noexcept { 161 | if (message == WM_NCCREATE) { 162 | auto window_struct = reinterpret_cast(lparam); 163 | SetWindowLongPtr(window, GWLP_USERDATA, 164 | reinterpret_cast(window_struct->lpCreateParams)); 165 | 166 | auto that = static_cast(window_struct->lpCreateParams); 167 | EnableFullDpiSupportIfAvailable(window); 168 | that->window_handle_ = window; 169 | } else if (Win32Window* that = GetThisFromHandle(window)) { 170 | return that->MessageHandler(window, message, wparam, lparam); 171 | } 172 | 173 | return DefWindowProc(window, message, wparam, lparam); 174 | } 175 | 176 | LRESULT 177 | Win32Window::MessageHandler(HWND hwnd, 178 | UINT const message, 179 | WPARAM const wparam, 180 | LPARAM const lparam) noexcept { 181 | switch (message) { 182 | case WM_DESTROY: 183 | window_handle_ = nullptr; 184 | Destroy(); 185 | if (quit_on_close_) { 186 | PostQuitMessage(0); 187 | } 188 | return 0; 189 | 190 | case WM_DPICHANGED: { 191 | auto newRectSize = reinterpret_cast(lparam); 192 | LONG newWidth = newRectSize->right - newRectSize->left; 193 | LONG newHeight = newRectSize->bottom - newRectSize->top; 194 | 195 | SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, 196 | newHeight, SWP_NOZORDER | SWP_NOACTIVATE); 197 | 198 | return 0; 199 | } 200 | case WM_SIZE: { 201 | RECT rect = GetClientArea(); 202 | if (child_content_ != nullptr) { 203 | // Size and position the child window. 204 | MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, 205 | rect.bottom - rect.top, TRUE); 206 | } 207 | return 0; 208 | } 209 | 210 | case WM_ACTIVATE: 211 | if (child_content_ != nullptr) { 212 | SetFocus(child_content_); 213 | } 214 | return 0; 215 | 216 | case WM_DWMCOLORIZATIONCOLORCHANGED: 217 | UpdateTheme(hwnd); 218 | return 0; 219 | } 220 | 221 | return DefWindowProc(window_handle_, message, wparam, lparam); 222 | } 223 | 224 | void Win32Window::Destroy() { 225 | OnDestroy(); 226 | 227 | if (window_handle_) { 228 | DestroyWindow(window_handle_); 229 | window_handle_ = nullptr; 230 | } 231 | if (g_active_window_count == 0) { 232 | WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); 233 | } 234 | } 235 | 236 | Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { 237 | return reinterpret_cast( 238 | GetWindowLongPtr(window, GWLP_USERDATA)); 239 | } 240 | 241 | void Win32Window::SetChildContent(HWND content) { 242 | child_content_ = content; 243 | SetParent(content, window_handle_); 244 | RECT frame = GetClientArea(); 245 | 246 | MoveWindow(content, frame.left, frame.top, frame.right - frame.left, 247 | frame.bottom - frame.top, true); 248 | 249 | SetFocus(child_content_); 250 | } 251 | 252 | RECT Win32Window::GetClientArea() { 253 | RECT frame; 254 | GetClientRect(window_handle_, &frame); 255 | return frame; 256 | } 257 | 258 | HWND Win32Window::GetHandle() { 259 | return window_handle_; 260 | } 261 | 262 | void Win32Window::SetQuitOnClose(bool quit_on_close) { 263 | quit_on_close_ = quit_on_close; 264 | } 265 | 266 | bool Win32Window::OnCreate() { 267 | // No-op; provided for subclasses. 268 | return true; 269 | } 270 | 271 | void Win32Window::OnDestroy() { 272 | // No-op; provided for subclasses. 273 | } 274 | 275 | void Win32Window::UpdateTheme(HWND const window) { 276 | DWORD light_mode; 277 | DWORD light_mode_size = sizeof(light_mode); 278 | LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, 279 | kGetPreferredBrightnessRegValue, 280 | RRF_RT_REG_DWORD, nullptr, &light_mode, 281 | &light_mode_size); 282 | 283 | if (result == ERROR_SUCCESS) { 284 | BOOL enable_dark_mode = light_mode == 0; 285 | DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, 286 | &enable_dark_mode, sizeof(enable_dark_mode)); 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /lib/builders/script_builder.dart: -------------------------------------------------------------------------------- 1 | /// This file contains the [ScriptBuilder] class, which is responsible for generating 2 | /// the Inno Setup Script (ISS) file for creating the installer of an application. 3 | /// 4 | /// The [ScriptBuilder] class takes a [Config] object and an [appDir] directory as inputs 5 | /// and generates the ISS script based on the provided configuration. This script defines 6 | /// various sections like setup, files, icons, tasks, and languages, among others. 7 | /// 8 | /// Key methods: 9 | /// - [_setup]: Generates the `[Setup]` section of the ISS script. 10 | /// - [_installDelete]: Generates the `[InstallDelete]` section. 11 | /// - [_languages]: Generates the `[Languages]` section. 12 | /// - [_tasks]: Generates the `[Tasks]` section. 13 | /// - [_files]: Generates the `[Files]` section. 14 | /// - [_icons]: Generates the `[Icons]` section. 15 | /// - [_run]: Generates the `[Run]` section. 16 | /// 17 | /// The [build] method is the main method of this class, which combines all the sections and writes 18 | /// the complete ISS script to a file. It returns the generated script file. 19 | library; 20 | 21 | import 'dart:io'; 22 | 23 | import 'package:inno_bundle/models/file_entry.dart'; 24 | import 'package:path/path.dart' as p; 25 | 26 | import 'package:inno_bundle/models/admin_mode.dart'; 27 | import 'package:inno_bundle/models/config.dart'; 28 | import 'package:inno_bundle/models/vcredist_mode.dart'; 29 | import 'package:inno_bundle/utils/cli_logger.dart'; 30 | import 'package:inno_bundle/utils/constants.dart'; 31 | import 'package:inno_bundle/utils/functions.dart'; 32 | 33 | /// A class responsible for generating the Inno Setup Script (ISS) file for the installer. 34 | class ScriptBuilder { 35 | /// The configuration guiding the script generation process. 36 | final Config config; 37 | 38 | /// The directory containing the application files to be included in the installer. 39 | final Directory appDir; 40 | 41 | /// Creates a [ScriptBuilder] instance with the given [config] and [appDir]. 42 | ScriptBuilder(this.config, this.appDir); 43 | 44 | /// Generates the `[Setup]` section of the ISS script, containing metadata and 45 | /// configuration for the installer. 46 | String _setup() { 47 | final outputDir = p.joinAll([ 48 | Directory.current.path, 49 | ...installerBuildDir, 50 | config.type.dirName, 51 | ]); 52 | 53 | var installerIcon = config.installerIcon; 54 | // save default icon into temp directory to use its path. 55 | if (installerIcon == defaultInstallerIconPlaceholder) { 56 | final installerIconDirPath = p.joinAll([ 57 | Directory.systemTemp.absolute.path, 58 | "${camelCase(config.name)}Installer", 59 | ]); 60 | installerIcon = persistDefaultInstallerIcon(installerIconDirPath); 61 | } 62 | 63 | return ''' 64 | [Setup] 65 | AppId=${config.id} 66 | AppName=${config.name} 67 | UninstallDisplayName=${config.name} 68 | UninstallDisplayIcon={app}\\${config.exePubspecName} 69 | AppVersion=${config.version} 70 | AppPublisher=${config.publisher} 71 | AppPublisherURL=${config.url} 72 | AppSupportURL=${config.supportUrl} 73 | AppUpdatesURL=${config.updatesUrl} 74 | LicenseFile=${config.licenseFile} 75 | DefaultDirName={autopf}\\${config.name} 76 | PrivilegesRequired=${config.admin == AdminMode.nonAdmin ? 'lowest' : 'admin'} 77 | PrivilegesRequiredOverridesAllowed=${config.admin == AdminMode.auto ? "dialog commandline" : ""} 78 | OutputDir=$outputDir 79 | OutputBaseFilename=${camelCase(config.name)}-${config.arch.cpu}-${config.version}-Installer 80 | SetupIconFile=$installerIcon 81 | Compression=lzma2/max 82 | SolidCompression=yes 83 | WizardStyle=modern 84 | ArchitecturesAllowed=${config.arch.value} 85 | ArchitecturesInstallIn64BitMode=${config.arch.value} 86 | DisableDirPage=auto 87 | DisableProgramGroupPage=auto 88 | ${config.signTool != null ? config.signTool?.toInnoCode() : ""} 89 | \n'''; 90 | } 91 | 92 | /// Generates the `[InstallDelete]` section for specifying files to delete during uninstallation. 93 | String _installDelete() { 94 | return ''' 95 | [InstallDelete] 96 | Type: filesandordirs; Name: "{app}\\*" 97 | \n'''; 98 | } 99 | 100 | /// Generates the `[Languages]` section, defining the languages supported by the installer. 101 | String _languages() { 102 | String section = "[Languages]\n"; 103 | for (final language in config.languages) { 104 | section += '${language.innoEntry}\n'; 105 | } 106 | return '$section\n'; 107 | } 108 | 109 | /// Generates the `[Tasks]` section, defining additional installation tasks such as creating desktop icons. 110 | String _tasks() { 111 | return ''' 112 | [Tasks] 113 | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; 114 | \n'''; 115 | } 116 | 117 | /// Generates the `[Files]` section, specifying the files and directories to include in the installer. 118 | String _files() { 119 | var section = "[Files]\n"; 120 | 121 | // adding app build files 122 | final appFiles = appDir.listSync(); 123 | 124 | for (final appFile in appFiles) { 125 | final filePath = appFile.absolute.path; 126 | if (FileSystemEntity.isDirectorySync(filePath)) { 127 | final fileName = p.basename(filePath); 128 | section += 'Source: "$filePath\\*"; DestDir: "{app}\\$fileName"; ' 129 | 'Flags: ignoreversion recursesubdirs createallsubdirs\n'; 130 | } else { 131 | section += 'Source: "$filePath"; DestDir: "{app}"; ' 132 | 'Flags: ignoreversion\n'; 133 | } 134 | } 135 | 136 | // adding optional files from System32 (if they are available), 137 | // so that the end user is not required to install 138 | // MS Visual C++ redistributable to run the app. 139 | final files = config.files.toList(); 140 | if (config.vcRedist == VcRedistMode.bundle) { 141 | files.addAll(FileEntry.vcEntries); 142 | } 143 | 144 | // copy all the files to the installer build directory 145 | final scriptDirPath = p.joinAll([ 146 | Directory.systemTemp.absolute.path, 147 | "${camelCase(config.name)}Installer", 148 | config.type.dirName, 149 | ]); 150 | Directory(scriptDirPath).createSync(recursive: true); 151 | 152 | for (final f in files) { 153 | final file = File(f.absolutePath); 154 | if (!file.existsSync()) { 155 | // if the file is not required, skip it, otherwise exit with error. 156 | if (!f.required) continue; 157 | CliLogger.exitError("Required file ${file.path} does not exist."); 158 | } 159 | 160 | final fPath = p.join(scriptDirPath, p.basename(file.path)); 161 | final fName = f.name; 162 | file.copySync(fPath); 163 | final destDir = p.join("{app}", f.destination ?? ""); 164 | section += 'Source: "$fPath"; DestDir: "$destDir"; ' 165 | 'DestName: "$fName"; Flags: ignoreversion\n'; 166 | } 167 | 168 | return '$section\n'; 169 | } 170 | 171 | /// Generates the `[Icons]` section, defining the shortcuts for the installed application. 172 | String _icons() { 173 | return ''' 174 | [Icons] 175 | Name: "{autoprograms}\\${config.name}"; Filename: "{app}\\${config.exePubspecName}" 176 | Name: "{autodesktop}\\${config.name}"; Filename: "{app}\\${config.exePubspecName}"; Tasks: desktopicon 177 | \n'''; 178 | } 179 | 180 | /// Generates the `[Run]` section, specifying actions to perform after the installation is complete. 181 | String _run() { 182 | return ''' 183 | [Run] 184 | Filename: "{app}\\${config.exePubspecName}"; Description: "{cm:LaunchProgram,{#StringChange('${config.name}', '&', '&&')}}"; Flags: nowait postinstall skipifsilent 185 | \n'''; 186 | } 187 | 188 | /// Generates the `[DownloadVcRedist]` section for downloading the Visual C++ Redistributable. 189 | /// This section will create a checkbox on the last page of the installer, checked it by default, 190 | /// if left checked after user click finish, 191 | /// the installer will open default browser and download the Visual C++ Redistributable 192 | String _downloadVcRedist() { 193 | if (config.vcRedist != VcRedistMode.download) return ''; 194 | return ''' 195 | ; This section will create a checkbox on the last page of the installer, checked it by default, 196 | ; if left checked after user click finish, 197 | ; the installer will open default browser and download the Visual C++ Redistributable 198 | [Code] 199 | var 200 | RunList: TNewCheckListBox; 201 | VCCheckBox: TNewCheckBox; 202 | 203 | // Repositions the VCCheckBox relative to the RunList's current position when the window is resized 204 | procedure OnWizardFormResize(Sender: TObject); 205 | begin 206 | if (VCCheckBox <> nil) and (RunList <> nil) and WizardForm.FinishedPage.Visible then 207 | begin 208 | // Reposition the checkbox relative to the RunList's current position 209 | VCCheckBox.SetBounds( 210 | RunList.Left + 4, 211 | RunList.Top + 25, 212 | VCCheckBox.Width, 213 | VCCheckBox.Height 214 | ); 215 | end; 216 | end; 217 | 218 | // Create checkbox on the last page of the installer to download VC Runtime. 219 | procedure CurPageChanged(CurPageID: Integer); 220 | begin 221 | if CurPageID = wpFinished then 222 | begin 223 | // The RunList is a TNewCheckListBox that contains checkboxes for [Run] entries 224 | // It's created automatically when you have [Run] entries with postinstall flag 225 | RunList := WizardForm.RunList; 226 | 227 | if RunList <> nil then 228 | begin 229 | // Create our custom checkbox below the RunList 230 | VCCheckBox := TNewCheckBox.Create(WizardForm); 231 | VCCheckBox.Parent := WizardForm.FinishedPage; 232 | 233 | // Position it below the RunList 234 | VCCheckBox.SetBounds( 235 | RunList.Left + 4, 236 | RunList.Top + 25, 237 | RunList.Width, 238 | ScaleY(17) 239 | ); 240 | 241 | // Set the checkbox properties 242 | VCCheckBox.Caption := 'Download and install required Visual C++ runtime (recommended)'; 243 | VCCheckBox.Checked := True; // Default checked 244 | VCCheckBox.Font.Assign(WizardForm.FinishedLabel.Font); 245 | 246 | // Make sure it's visible 247 | VCCheckBox.Visible := True; 248 | 249 | // Assign the resize event handler 250 | WizardForm.OnResize := @OnWizardFormResize; 251 | end 252 | end; 253 | end; 254 | 255 | // This runs *after* the user clicks "Finish" on the last page 256 | procedure CurStepChanged(CurStep: TSetupStep); 257 | var 258 | ErrCode: Integer; 259 | begin 260 | if (CurStep = ssDone) and VCCheckBox.Checked then 261 | begin 262 | ShellExec('', 'https://aka.ms/vs/17/release/vc_redist.x64.exe', '', '', SW_SHOWNORMAL, ewNoWait, ErrCode); 263 | end; 264 | end; 265 | \n'''; 266 | } 267 | 268 | /// Generates the ISS script file and returns its path. 269 | Future build() async { 270 | CliLogger.info("Generating ISS script..."); 271 | final script = scriptHeader + 272 | _setup() + 273 | _installDelete() + 274 | _languages() + 275 | _tasks() + 276 | _files() + 277 | _icons() + 278 | _run() + 279 | _downloadVcRedist(); 280 | final relScriptPath = p.joinAll([ 281 | ...installerBuildDir, 282 | config.type.dirName, 283 | "inno-script.iss", 284 | ]); 285 | final absScriptPath = p.join(Directory.current.path, relScriptPath); 286 | final scriptFile = File(absScriptPath); 287 | scriptFile.createSync(recursive: true); 288 | scriptFile.writeAsStringSync(script); 289 | CliLogger.success("Script generated $relScriptPath"); 290 | return scriptFile; 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /lib/models/config.dart: -------------------------------------------------------------------------------- 1 | /// This file contains the [Config] class, which represents the configuration 2 | /// for building a Windows installer using Inno Setup. 3 | /// 4 | /// The [Config] class holds various attributes necessary for the build process, 5 | /// including application-specific details such as ID, name, description, and version. 6 | /// It also includes build-related settings like the installer icon, languages, 7 | /// administrator mode, and whether to include the app or create an installer file. 8 | /// 9 | /// This file provides methods to create a [Config] instance from JSON or directly 10 | /// from the `pubspec.yaml` file and custom config file if provided, as well as 11 | /// a method to convert the configuration attributes into environment variables 12 | /// for further use. 13 | library; 14 | 15 | import 'dart:io'; 16 | 17 | import 'package:inno_bundle/models/admin_mode.dart'; 18 | import 'package:inno_bundle/models/build_arch.dart'; 19 | import 'package:inno_bundle/models/build_type.dart'; 20 | import 'package:inno_bundle/models/build_tool.dart'; 21 | import 'package:inno_bundle/models/cli_config.dart'; 22 | import 'package:inno_bundle/models/file_entry.dart'; 23 | import 'package:inno_bundle/models/language.dart'; 24 | import 'package:inno_bundle/models/sign_tool.dart'; 25 | import 'package:inno_bundle/models/vcredist_mode.dart'; 26 | import 'package:inno_bundle/utils/cli_logger.dart'; 27 | import 'package:inno_bundle/utils/constants.dart'; 28 | import 'package:inno_bundle/utils/functions.dart'; 29 | import 'package:path/path.dart' as p; 30 | import 'package:uuid/uuid.dart'; 31 | 32 | /// A class representing the configuration for building a Windows installer using Inno Setup. 33 | class Config { 34 | /// The unique identifier (UUID) for the app being packaged. 35 | final String id; 36 | 37 | /// The pubspec file sourced for this configuration, used as a fallback source of values. 38 | final File pubspecFile; 39 | 40 | /// The config file sourced for this configuration, used as the main source of values. 41 | final File configFile; 42 | 43 | /// The global pubspec name attribute, same name of the exe generated from flutter build. 44 | final String pubspecName; 45 | 46 | /// The name of the app after packaging. 47 | final String name; 48 | 49 | /// A description of the app being packaged. 50 | final String description; 51 | 52 | /// The app's version. 53 | final String version; 54 | 55 | /// The name of the publisher or maintainer. 56 | final String publisher; 57 | 58 | /// The app's homepage URL. 59 | final String url; 60 | 61 | /// The URL for support resources. 62 | final String supportUrl; 63 | 64 | /// The URL for checking for updates. 65 | final String updatesUrl; 66 | 67 | /// The path to the installer icon file. 68 | final String installerIcon; 69 | 70 | /// The path to the text license file. 71 | final String licenseFile; 72 | 73 | /// The name or commmand to be used to digitally sign the installer. 74 | final SignTool? signTool; 75 | 76 | /// The supported languages for the installer. 77 | final List languages; 78 | 79 | /// Whether the installer requires administrator privileges. 80 | final AdminMode admin; 81 | 82 | /// The build type (release, profile, or debug). 83 | final BuildType type; 84 | 85 | /// Whether to include the app in the installer. 86 | final bool app; 87 | 88 | /// Whether to create an installer file. 89 | final bool installer; 90 | 91 | /// CPU Architecture supported by the app and installer to run on. 92 | final BuildArch arch; 93 | 94 | /// Arguments to be passed to flutter build. 95 | final String? buildArgs; 96 | 97 | /// Arguments to be passed to shorebird release. 98 | final String? shorebirdArgs; 99 | 100 | /// The tool to use for building the app (flutter or shorebird). 101 | final BuildTool buildTool; 102 | 103 | /// The mode for handling the Visual C++ Redistributable. 104 | final VcRedistMode vcRedist; 105 | 106 | /// List of files to be included in the installer. 107 | final List files; 108 | 109 | /// Creates a [Config] instance with default values. 110 | const Config({ 111 | required this.pubspecFile, 112 | required this.configFile, 113 | required this.files, 114 | required this.buildArgs, 115 | required this.shorebirdArgs, 116 | required this.buildTool, 117 | required this.id, 118 | required this.pubspecName, 119 | required this.name, 120 | required this.description, 121 | required this.version, 122 | required this.publisher, 123 | required this.url, 124 | required this.supportUrl, 125 | required this.updatesUrl, 126 | required this.installerIcon, 127 | required this.languages, 128 | required this.admin, 129 | required this.licenseFile, 130 | required this.signTool, 131 | required this.arch, 132 | required this.vcRedist, 133 | this.type = BuildType.release, 134 | this.app = true, 135 | this.installer = true, 136 | }); 137 | 138 | /// The name of the executable file that is created with flutter build. 139 | String get exePubspecName => "$pubspecName.exe"; 140 | 141 | /// The name of the executable file that will be created. 142 | String get exeName => "$name.exe"; 143 | 144 | /// Creates a [Config] instance from a JSON map, typically read from `pubspec.yaml` and a config file (if provided). 145 | /// 146 | /// Validates the configuration and exits with an error if invalid values are found. 147 | factory Config.fromJson( 148 | Map json, 149 | Map configJson, { 150 | required CliConfig cliConfig, 151 | required File pubspecFile, 152 | required File configFile, 153 | }) { 154 | final configName = 155 | configFile == pubspecFile ? "pubspec.yaml" : "config file"; 156 | if (configJson['inno_bundle'] is! Map) { 157 | CliLogger.exitError("inno_bundle section is missing from $configName."); 158 | } 159 | final Map inno = configJson['inno_bundle']; 160 | 161 | if (inno['id'] is! String) { 162 | CliLogger.exitError( 163 | "inno_bundle.id attribute is missing from $configName. " 164 | "Run `dart run inno_bundle:guid` to generate a new one, " 165 | "then put it in your $configName."); 166 | } else if (!Uuid.isValidUUID(fromString: inno['id'])) { 167 | CliLogger.exitError("inno_bundle.id from $configName is not valid. " 168 | "Run `dart run inno_bundle:guid` to generate a new one, " 169 | "then put it in your $configName."); 170 | } 171 | final String id = inno['id']; 172 | 173 | if (json['name'] is! String) { 174 | CliLogger.exitError("name attribute is missing from $configName."); 175 | } 176 | final String pubspecName = json['name']; 177 | 178 | if (inno['name'] != null && !validFilenameRegex.hasMatch(inno['name'])) { 179 | CliLogger.exitError("inno_bundle.name from $configName is not valid. " 180 | "`${inno['name']}` is not a valid file name."); 181 | } 182 | final String name = inno['name'] ?? pubspecName; 183 | 184 | if ((cliConfig.appVersion ?? inno['version'] ?? json['version']) 185 | is! String) { 186 | CliLogger.exitError("version attribute is missing from $configName."); 187 | } 188 | final String version = 189 | cliConfig.appVersion ?? inno['version'] ?? json['version']; 190 | 191 | if ((inno['description'] ?? json['description']) is! String) { 192 | CliLogger.exitError("description attribute is missing from $configName."); 193 | } 194 | final String description = inno['description'] ?? json['description']; 195 | 196 | if ((inno['publisher'] ?? json['maintainer']) is! String) { 197 | CliLogger.exitError("maintainer or inno_bundle.publisher attributes are " 198 | "missing from $configName."); 199 | } 200 | final String publisher = inno['publisher'] ?? json['maintainer']; 201 | 202 | final url = (inno['url'] ?? json['homepage'] ?? "") as String; 203 | final supportUrl = (inno['support_url'] as String?) ?? url; 204 | final updatesUrl = (inno['updates_url'] as String?) ?? url; 205 | 206 | if (inno['installer_icon'] != null && inno['installer_icon'] is! String) { 207 | CliLogger.exitError("inno_bundle.installer_icon attribute is invalid " 208 | "in $configName."); 209 | } 210 | final installerIcon = inno['installer_icon'] != null 211 | ? p.join( 212 | Directory.current.path, 213 | p.fromUri(inno['installer_icon']), 214 | ) 215 | : defaultInstallerIconPlaceholder; 216 | if (installerIcon != defaultInstallerIconPlaceholder && 217 | !File(installerIcon).existsSync()) { 218 | CliLogger.exitError( 219 | "inno_bundle.installer_icon attribute value is invalid, " 220 | "`$installerIcon` file does not exist."); 221 | } 222 | 223 | if (inno['languages'] != null && inno['languages'] is! List) { 224 | CliLogger.exitError("inno_bundle.languages attribute is invalid " 225 | "in $configName, only a list of strings is allowed."); 226 | } 227 | final languages = (inno['languages'] as List?) 228 | ?.map((l) { 229 | final error = Language.validateConfig(l, configName: configName); 230 | if (error != null) CliLogger.exitError(error); 231 | final language = Language.getByNameOrNull(l); 232 | if (language == null) return null; 233 | return language; 234 | }) 235 | .whereType() 236 | .toList(growable: false) ?? 237 | Language.values; 238 | 239 | if (inno['admin'] != null && 240 | inno['admin'] is! bool && 241 | inno['admin'] != "auto") { 242 | CliLogger.exitError("inno_bundle.admin attribute is invalid value " 243 | "in $configName"); 244 | } 245 | final admin = AdminMode.fromOption(inno['admin'] ?? true); 246 | 247 | if (inno['license_file'] != null && inno['license_file'] is! String) { 248 | CliLogger.exitError("inno_bundle.license_file attribute is invalid " 249 | "in $configName."); 250 | } 251 | 252 | final licenseFilePath = p.join( 253 | Directory.current.path, 254 | inno['license_file'] != null 255 | ? p.fromUri(inno['license_file']) 256 | : 'LICENSE', 257 | ); 258 | final licenseFile = 259 | File(licenseFilePath).existsSync() ? licenseFilePath : ''; 260 | 261 | final signToolError = SignTool.validateConfig( 262 | inno["sign_tool"], 263 | configName: configName, 264 | signToolName: cliConfig.signToolName, 265 | signToolCommand: cliConfig.signToolCommand, 266 | signToolParams: cliConfig.signToolParams, 267 | ); 268 | if (signToolError != null) CliLogger.exitError(signToolError); 269 | final signTool = SignTool.fromOption( 270 | inno['sign_tool'], 271 | signToolName: cliConfig.signToolName, 272 | signToolCommand: cliConfig.signToolCommand, 273 | signToolParams: cliConfig.signToolParams, 274 | ); 275 | 276 | final archError = 277 | BuildArch.validateConfig(inno['arch'], configName: configName); 278 | if (archError != null) CliLogger.exitError(archError); 279 | final arch = BuildArch.fromOption(inno['arch']); 280 | 281 | if (inno['vc_redist'] != null && 282 | inno['vc_redist'] is! bool && 283 | inno['vc_redist'] != "download") { 284 | CliLogger.exitError("inno_bundle.vc_redist attribute is invalid value " 285 | "in $configName"); 286 | } 287 | final vcRedist = VcRedistMode.fromOption(inno['vc_redist'] ?? true); 288 | 289 | if (inno['dlls'] != null) { 290 | CliLogger.addDeferred( 291 | "inno_bundle.dlls attribute is deprecated, use inno_bundle.files instead.", 292 | kind: CliLoggerKind.warning, 293 | ); 294 | 295 | if (inno['dlls'] is! List) { 296 | CliLogger.exitError("inno_bundle.dlls attribute is invalid " 297 | "in $configName, only a list of dll entries is allowed."); 298 | } 299 | } 300 | 301 | if (inno['files'] != null && inno['files'] is! List) { 302 | CliLogger.exitError("inno_bundle.files attribute is invalid " 303 | "in $configName, only a list of file entries is allowed."); 304 | } 305 | 306 | // merge dlls and files for backward compatibility, in later versions, the dlls attribute will be removed. 307 | final files = [...inno['dlls'] ?? [], ...(inno['files'] ?? [])] 308 | .map((file) { 309 | if (file == null) return null; 310 | 311 | final e = FileEntry.validateConfig(file, configName: configName); 312 | if (e != null) CliLogger.exitError(e); 313 | 314 | return FileEntry.fromJson(file); 315 | }) 316 | .whereType() 317 | .toList(growable: false); 318 | 319 | return Config( 320 | pubspecFile: pubspecFile, 321 | configFile: configFile, 322 | buildArgs: cliConfig.buildArgs, 323 | shorebirdArgs: 324 | cliConfig.shorebirdArgs ?? (inno['shorebird_args'] as String?), 325 | buildTool: cliConfig.hasBuildToolArg 326 | ? cliConfig.buildTool 327 | : BuildTool.fromOption( 328 | inno['build_tool'] ?? cliConfig.buildTool.name), 329 | id: id, 330 | pubspecName: pubspecName, 331 | name: name, 332 | description: description, 333 | version: version, 334 | publisher: publisher, 335 | url: url, 336 | supportUrl: supportUrl, 337 | updatesUrl: updatesUrl, 338 | installerIcon: installerIcon, 339 | languages: languages, 340 | admin: admin, 341 | type: cliConfig.type, 342 | app: cliConfig.app, 343 | installer: cliConfig.installer, 344 | licenseFile: licenseFile, 345 | signTool: signTool, 346 | arch: arch, 347 | vcRedist: vcRedist, 348 | files: files, 349 | ); 350 | } 351 | 352 | /// Creates a [Config] instance directly from the `pubspec.yaml` file and a config file (if provided). 353 | /// 354 | /// Provides a convenient way to load configuration without manual JSON parsing. 355 | factory Config.fromFile( 356 | File pubspecFile, 357 | File configFile, 358 | CliConfig cliConfig, 359 | ) { 360 | final pubspecJson = readYaml(pubspecFile); 361 | final configJson = 362 | configFile == pubspecFile ? pubspecJson : readYaml(configFile); 363 | 364 | return Config.fromJson( 365 | pubspecJson, 366 | configJson, 367 | cliConfig: cliConfig, 368 | pubspecFile: pubspecFile, 369 | configFile: configFile, 370 | ); 371 | } 372 | 373 | /// Returns a string containing the config attributes as environment variables. 374 | String toEnvironmentVariables() { 375 | final variables = { 376 | 'APP_ID': id, 377 | 'PUBSPEC_NAME': pubspecName, 378 | 'CONFIG_FILE': configFile.path, 379 | 'APP_NAME': name, 380 | 'APP_NAME_CAMEL_CASE': camelCase(name), 381 | 'APP_DESCRIPTION': description, 382 | 'APP_VERSION': version, 383 | 'APP_PUBLISHER': publisher, 384 | 'APP_URL': url, 385 | 'APP_SUPPORT_URL': supportUrl, 386 | 'APP_UPDATES_URL': updatesUrl, 387 | 'APP_INSTALLER_ICON': installerIcon, 388 | 'APP_LANGUAGES': languages.map((l) => l.name).join(','), 389 | 'APP_ADMIN': admin.toString(), 390 | 'APP_VCREDIST': vcRedist.toString(), 391 | 'APP_TYPE': type.name, 392 | 'APP_BUILD_APP': app.toString(), 393 | 'APP_BUILD_INSTALLER': installer.toString(), 394 | }; 395 | 396 | return variables.entries 397 | .map((entry) => '${entry.key}=${entry.value}') 398 | .join('\n'); 399 | } 400 | } 401 | --------------------------------------------------------------------------------