ann-flutter-flavor 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +271 -0
- data/Rakefile +6 -0
- data/ann-flutter-flavor.gemspec +25 -0
- data/lib/ann-flutter-flavor.rb +10 -0
- data/lib/ann_flutter_flavor/version.rb +3 -0
- data/lib/fastlane/plugin/ann_flutter_flavor/actions/ann_compile_build_action.rb +219 -0
- data/lib/fastlane/plugin/ann_flutter_flavor/actions/ann_download_from_app_store_action.rb +183 -0
- data/lib/fastlane/plugin/ann_flutter_flavor/actions/ann_download_from_play_store_action.rb +168 -0
- data/lib/fastlane/plugin/ann_flutter_flavor/actions/ann_emulators_action.rb +180 -0
- data/lib/fastlane/plugin/ann_flutter_flavor/actions/ann_run_tests_action.rb +128 -0
- data/lib/fastlane/plugin/ann_flutter_flavor/actions/ann_setup_action.rb +137 -0
- data/lib/fastlane/plugin/ann_flutter_flavor/actions/ann_upload_to_app_store_action.rb +280 -0
- data/lib/fastlane/plugin/ann_flutter_flavor/actions/ann_upload_to_firebase_action.rb +199 -0
- data/lib/fastlane/plugin/ann_flutter_flavor/actions/ann_upload_to_play_store_action.rb +248 -0
- data/lib/fastlane/plugin/ann_flutter_flavor/helper/lanes_android.rb +120 -0
- data/lib/fastlane/plugin/ann_flutter_flavor/helper/lanes_annai.rb +502 -0
- data/lib/fastlane/plugin/ann_flutter_flavor/helper/lanes_ios.rb +157 -0
- data/lib/fastlane/plugin/ann_flutter_flavor/helper/lanes_setup.rb +161 -0
- data/lib/fastlane/plugin/ann_flutter_flavor/helper/podspec_bridge.rb +153 -0
- data/lib/fastlane/plugin/ann_flutter_flavor/helper/test_integration.rb +346 -0
- data/lib/fastlane/plugin/ann_flutter_flavor/helper/utils_project_config.rb +96 -0
- data/lib/fastlane/plugin/ann_flutter_flavor/helper/utils_spec_loader.rb +363 -0
- data/lib/fastlane/plugin/ann_flutter_flavor/helper/utils_status.rb +115 -0
- data/lib/fastlane/plugin/ann_flutter_flavor.rb +23 -0
- data/plugin_manager.sh +140 -0
- metadata +112 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
require 'fastlane/action'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
|
|
4
|
+
module FastlaneFlutterFlavor
|
|
5
|
+
# ------------------------------------
|
|
6
|
+
# ProjectUtil (Helper Module)
|
|
7
|
+
# Contains logic to locate the Flutter project root directory and key config files.
|
|
8
|
+
# ------------------------------------
|
|
9
|
+
module ProjectUtil
|
|
10
|
+
|
|
11
|
+
# ===============================================
|
|
12
|
+
# Find Flutter Project Root
|
|
13
|
+
# Searches upwards from the current directory for a pubspec.yaml file.
|
|
14
|
+
# ===============================================
|
|
15
|
+
def self.find_flutter_root
|
|
16
|
+
current_dir = Dir.pwd
|
|
17
|
+
while current_dir != '/' do
|
|
18
|
+
pubspec_path = File.join(current_dir, 'pubspec.yaml')
|
|
19
|
+
if File.exist?(pubspec_path)
|
|
20
|
+
Fastlane::UI.verbose("Found pubspec.yaml at: #{current_dir}")
|
|
21
|
+
return current_dir
|
|
22
|
+
end
|
|
23
|
+
parent_dir = File.dirname(current_dir)
|
|
24
|
+
# Prevent infinite loop if we hit the root and haven't found it
|
|
25
|
+
break if parent_dir == current_dir
|
|
26
|
+
current_dir = parent_dir
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Fallback to the original logic if pubspec.yaml isn't found
|
|
30
|
+
Fastlane::UI.important("pubspec.yaml not found by searching up. Falling back to current directory.")
|
|
31
|
+
return Dir.pwd
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# ===============================================
|
|
35
|
+
# Internal method to resolve config path with precedence
|
|
36
|
+
# Precedence: User Parameter > Environment Variable > Default Path
|
|
37
|
+
#
|
|
38
|
+
# ===============================================
|
|
39
|
+
def self.resolve_config_path(filename, user_defined_path, env_var_name)
|
|
40
|
+
chosen_path_value = nil
|
|
41
|
+
source = "default in project root"
|
|
42
|
+
|
|
43
|
+
# Precedence 1: User-passed parameter (from action/lane)
|
|
44
|
+
# Check if user passed an explicit path (i.e., not the action's default filename string)
|
|
45
|
+
if user_defined_path && user_defined_path != filename
|
|
46
|
+
chosen_path_value = user_defined_path
|
|
47
|
+
source = "user parameter"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Precedence 2: Environment Variable
|
|
51
|
+
if chosen_path_value.nil?
|
|
52
|
+
env_path = ENV[env_var_name]
|
|
53
|
+
if env_path && !env_path.empty?
|
|
54
|
+
chosen_path_value = env_path
|
|
55
|
+
source = "environment variable (#{env_var_name})"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Precedence 3: Default in Project Root
|
|
60
|
+
if chosen_path_value.nil?
|
|
61
|
+
root = find_flutter_root
|
|
62
|
+
chosen_path_value = File.join(root, filename)
|
|
63
|
+
# source remains "default in project root"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Resolve the path and check for existence
|
|
67
|
+
resolved_path = File.expand_path(chosen_path_value)
|
|
68
|
+
|
|
69
|
+
if File.exist?(resolved_path)
|
|
70
|
+
Fastlane::UI.verbose("Found #{filename} path from #{source}: #{resolved_path}")
|
|
71
|
+
else
|
|
72
|
+
Fastlane::UI.warning("#{filename} file not found at resolved path: #{resolved_path} (Source: #{source}).")
|
|
73
|
+
# Note: We still return the path, as it might be created later or is only referenced for metadata.
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
return resolved_path
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ===============================================
|
|
81
|
+
# Find Annai Spec File Path (annspec.yaml)
|
|
82
|
+
# Uses ANN_SPEC_FILE environment variable.
|
|
83
|
+
# ===============================================
|
|
84
|
+
def self.find_annai_spec_path(user_defined_path = nil)
|
|
85
|
+
return resolve_config_path('annspec.yaml', user_defined_path, 'ANN_SPEC_FILE')
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# ===============================================
|
|
89
|
+
# Find Annai Test Spec File Path (annaitestspec.yaml)
|
|
90
|
+
# Uses ANNAI_TEST_SPEC_FILE environment variable.
|
|
91
|
+
# ===============================================
|
|
92
|
+
def self.find_annai_test_spec_path(user_defined_path = nil)
|
|
93
|
+
return resolve_config_path('annaitestspec.yaml', user_defined_path, 'ANNAI_TEST_SPEC_FILE')
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
require 'yaml'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
require 'fastlane/action'
|
|
4
|
+
|
|
5
|
+
module FastlaneFlutterFlavor
|
|
6
|
+
|
|
7
|
+
class YamlSpecLoader
|
|
8
|
+
|
|
9
|
+
# Initialize the loader with the project root folder context and load the spec file.
|
|
10
|
+
def initialize(root_folder, file_path)
|
|
11
|
+
@root_folder = root_folder
|
|
12
|
+
@file_path = file_path # The file path, potentially overridden by ENV outside this class
|
|
13
|
+
|
|
14
|
+
# Load the file content immediately and store it
|
|
15
|
+
@spec_data = load_file
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Loads and parses the raw YAML spec file using the instance file path.
|
|
19
|
+
# @return [Hash, nil] The parsed YAML data, or nil on error.
|
|
20
|
+
def load_file
|
|
21
|
+
begin
|
|
22
|
+
# Ensure the file path is resolved relative to the project root
|
|
23
|
+
full_path = File.expand_path(@file_path, @root_folder)
|
|
24
|
+
Fastlane::UI.verbose("Reading Annai spec from: #{full_path}")
|
|
25
|
+
|
|
26
|
+
# Use safe_load to prevent arbitrary code execution from YAML files
|
|
27
|
+
yaml_content = YAML.safe_load(File.read(full_path), aliases: true)
|
|
28
|
+
|
|
29
|
+
# Ensure the content is a Hash before returning
|
|
30
|
+
unless yaml_content.is_a?(Hash)
|
|
31
|
+
raise "YAML content must be a top-level Hash."
|
|
32
|
+
end
|
|
33
|
+
return yaml_content
|
|
34
|
+
rescue => e
|
|
35
|
+
# Use @file_path for reporting the error path
|
|
36
|
+
Fastlane::UI.error("Failed to load or parse annai spec YAML file at #{@file_path}: #{e.message}")
|
|
37
|
+
return nil
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Recursively merges defaults into a flavor's data.
|
|
42
|
+
# This function prioritizes 'current_data' (the flavor-specific value).
|
|
43
|
+
# If a key is present in 'default_data' but missing in 'current_data',
|
|
44
|
+
# the default value is used.
|
|
45
|
+
#
|
|
46
|
+
# @param current_data [Object] The flavor-specific data (Hash or scalar).
|
|
47
|
+
# @param default_data [Object] The default data to merge (Hash or scalar).
|
|
48
|
+
# @return [Object] The merged data.
|
|
49
|
+
def deep_merge_defaults(current_data, default_data)
|
|
50
|
+
|
|
51
|
+
# FIX: If current_data is a scalar, it means the flavor explicitly set this value.
|
|
52
|
+
# It overrides the default, so we return it immediately.
|
|
53
|
+
return current_data unless current_data.is_a?(Hash)
|
|
54
|
+
|
|
55
|
+
# Base case 2: If default_data is not a hash, there's nothing left to merge deeply.
|
|
56
|
+
return current_data unless default_data.is_a?(Hash)
|
|
57
|
+
|
|
58
|
+
# Recursive case: Both are hashes, perform the merge
|
|
59
|
+
default_data.each do |key, default_value|
|
|
60
|
+
current_value = current_data[key]
|
|
61
|
+
|
|
62
|
+
if current_data.key?(key)
|
|
63
|
+
# Key exists in flavor (current_data), recursively merge
|
|
64
|
+
current_data[key] = deep_merge_defaults(current_value, default_value)
|
|
65
|
+
else
|
|
66
|
+
# The key is missing in the current flavor data. Use the default value.
|
|
67
|
+
current_data[key] = default_value
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
return current_data
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Retrieves the configuration for a specific flavor, merged with the platform defaults.
|
|
75
|
+
# This function uses the spec data loaded into the instance variable @spec_data.
|
|
76
|
+
#
|
|
77
|
+
# @param platform [Symbol] The target platform (:ios or :android).
|
|
78
|
+
# @param flavor_name [String] The name of the flavor (e.g., 'dev', 'staging').
|
|
79
|
+
# @return [Hash, nil] The fully merged configuration for the flavor, or nil if missing.
|
|
80
|
+
def get_merged_flavor_config(platform, flavor_name)
|
|
81
|
+
return nil unless @spec_data # Check if file was loaded successfully
|
|
82
|
+
|
|
83
|
+
platform_key = platform.to_s
|
|
84
|
+
flavor_key = flavor_name.to_s
|
|
85
|
+
|
|
86
|
+
# 1. Extract platform data from instance spec data
|
|
87
|
+
platform_data = @spec_data.dig('annai_app', platform_key)
|
|
88
|
+
unless platform_data.is_a?(Hash)
|
|
89
|
+
Fastlane::UI.verbose("Annai spec missing 'annai_app' or platform '#{platform_key}'.")
|
|
90
|
+
return nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# 2. Extract specific flavor data and defaults
|
|
94
|
+
flavor_data = platform_data.dig('flavor', flavor_key)
|
|
95
|
+
default_data = platform_data.dig('default')
|
|
96
|
+
|
|
97
|
+
# If flavor data is missing entirely, and we can't merge, return nil
|
|
98
|
+
if flavor_data.nil? && default_data.nil?
|
|
99
|
+
Fastlane::UI.verbose("Flavor '#{flavor_key}' and platform default config are missing for platform '#{platform_key}'.")
|
|
100
|
+
return nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# 3. Deep merge the flavor data (current) over the default data (base)
|
|
104
|
+
current_config = flavor_data || {}
|
|
105
|
+
|
|
106
|
+
# Perform the merge operation. We clone current_config to avoid modifying the original spec hash.
|
|
107
|
+
merged_config = deep_merge_defaults(current_config.dup, default_data || {})
|
|
108
|
+
|
|
109
|
+
# Add the flavor name itself for consistency in the returned config hash
|
|
110
|
+
merged_config['flavor'] = flavor_key
|
|
111
|
+
|
|
112
|
+
return merged_config
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# --- GETTER WRAPPERS ---
|
|
116
|
+
|
|
117
|
+
# Retrieves the hash of all defined flavors for a given platform.
|
|
118
|
+
# Uses the instance spec data.
|
|
119
|
+
#
|
|
120
|
+
# @param platform [Symbol] The target platform (:ios or :android).
|
|
121
|
+
# @return [Hash] A hash where keys are flavor names (String) and values are their raw configs (Hash).
|
|
122
|
+
def get_platform_flavors(platform)
|
|
123
|
+
return {} unless @spec_data
|
|
124
|
+
platform_key = platform.to_s
|
|
125
|
+
|
|
126
|
+
# Use dig to safely retrieve the flavors hash from @spec_data
|
|
127
|
+
flavors = @spec_data.dig('annai_app', platform_key, 'flavor')
|
|
128
|
+
|
|
129
|
+
# Return the flavors hash, or an empty hash if no flavors are defined
|
|
130
|
+
return flavors.is_a?(Hash) ? flavors : {}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# @return [String, nil] The flavor name (just for consistency with other getters).
|
|
134
|
+
def get_flavor_name(platform, flavor_name)
|
|
135
|
+
config = get_merged_flavor_config(platform, flavor_name)
|
|
136
|
+
config.dig('flavor') if config
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# @return [String, nil] The merged package ID (bundle ID or application ID).
|
|
140
|
+
# It combines 'id' and 'id_suffix' (if present) via direct concatenation.
|
|
141
|
+
# NOTE: It is assumed that the 'id_suffix' defined in the configuration
|
|
142
|
+
def get_package_id(platform, flavor_name)
|
|
143
|
+
config = get_merged_flavor_config(platform, flavor_name)
|
|
144
|
+
return nil unless config
|
|
145
|
+
|
|
146
|
+
base_id = config.dig('id')
|
|
147
|
+
return nil unless base_id
|
|
148
|
+
|
|
149
|
+
# Check for an optional suffix
|
|
150
|
+
suffix = config.dig('id_suffix')
|
|
151
|
+
|
|
152
|
+
# If a suffix is present, combine it directly with the base ID.
|
|
153
|
+
# This relies on the suffix string itself containing the correct separator (like ".")
|
|
154
|
+
# to correctly form the final package ID (e.g., "com.base" + ".dev" -> "com.base.dev").
|
|
155
|
+
if suffix && !suffix.empty?
|
|
156
|
+
return base_id + suffix
|
|
157
|
+
else
|
|
158
|
+
# Return the base ID if no suffix is present.
|
|
159
|
+
return base_id
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# @return [String, nil] The merged app name.
|
|
164
|
+
# It combines 'name' and 'name_suffix' (if present) via direct concatenation.
|
|
165
|
+
# NOTE: It is assumed that the 'name_suffix' defined in the configuration
|
|
166
|
+
def get_display_name(platform, flavor_name)
|
|
167
|
+
config = get_merged_flavor_config(platform, flavor_name)
|
|
168
|
+
return nil unless config
|
|
169
|
+
|
|
170
|
+
base_name = config.dig('name')
|
|
171
|
+
return nil unless base_name
|
|
172
|
+
|
|
173
|
+
# Check for an optional suffix
|
|
174
|
+
suffix = config.dig('name_suffix')
|
|
175
|
+
|
|
176
|
+
# If a suffix is present, combine it directly with the base Name.
|
|
177
|
+
# This relies on the suffix string itself containing the correct separator
|
|
178
|
+
# to correctly form the final Name (e.g., "Test" + " App" -> "Test App").
|
|
179
|
+
if suffix && !suffix.empty?
|
|
180
|
+
return base_name + suffix
|
|
181
|
+
else
|
|
182
|
+
# Return the base Name if no suffix is present.
|
|
183
|
+
return base_name
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# @return [String, nil] The merged version name.
|
|
188
|
+
# It combines 'version_name' and 'version_name_suffix' (if present) via direct concatenation.
|
|
189
|
+
# NOTE: It is assumed that the 'version_name_suffix' defined in the configuration
|
|
190
|
+
def get_version_name(platform, flavor_name)
|
|
191
|
+
config = get_merged_flavor_config(platform, flavor_name)
|
|
192
|
+
return nil unless config
|
|
193
|
+
|
|
194
|
+
base_version_name = config.dig('version_name')
|
|
195
|
+
return nil unless base_version_name
|
|
196
|
+
|
|
197
|
+
# Check for an optional suffix
|
|
198
|
+
suffix = config.dig('version_name_suffix')
|
|
199
|
+
|
|
200
|
+
# If a suffix is present, combine it directly with the base version name.
|
|
201
|
+
# This relies on the suffix string itself containing the correct separator
|
|
202
|
+
# to correctly form the final version name (e.g., "V" + "1.0.1" -> "V1.0.1").
|
|
203
|
+
if suffix && !suffix.empty?
|
|
204
|
+
return base_version_name + suffix
|
|
205
|
+
else
|
|
206
|
+
# Return the base version name if no suffix is present.
|
|
207
|
+
return base_version_name
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# @return [String, nil] The path to the version_code.
|
|
212
|
+
def get_version_code(platform, flavor_name)
|
|
213
|
+
config = get_merged_flavor_config(platform, flavor_name)
|
|
214
|
+
if config
|
|
215
|
+
version_code_value = config.dig('version_code').to_s.strip
|
|
216
|
+
return version_code_value.to_i(10).to_s
|
|
217
|
+
end
|
|
218
|
+
return nil
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# @return [String, nil] The path to the main Dart file.
|
|
222
|
+
def get_main_file(platform, flavor_name)
|
|
223
|
+
config = get_merged_flavor_config(platform, flavor_name)
|
|
224
|
+
config.dig('main_file') if config
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# @return [String, nil] The priority level.
|
|
228
|
+
def get_priority(platform, flavor_name)
|
|
229
|
+
config = get_merged_flavor_config(platform, flavor_name)
|
|
230
|
+
config.dig('priority') if config
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# @return [String, nil] The API key path (e.g., path to Google Service Account JSON or App Store Connect API Key JSON).
|
|
234
|
+
def get_api_key_path(platform, flavor_name)
|
|
235
|
+
# 1. Get the merged configuration for the specific flavor
|
|
236
|
+
config = get_merged_flavor_config(platform, flavor_name)
|
|
237
|
+
|
|
238
|
+
# Return nil early if configuration is missing
|
|
239
|
+
return nil unless config
|
|
240
|
+
|
|
241
|
+
# 2. Extract the key based on the platform
|
|
242
|
+
case platform
|
|
243
|
+
when :android
|
|
244
|
+
# For Android, we look for 'google_api_key'
|
|
245
|
+
config.dig('fastlane', 'google_api_key')
|
|
246
|
+
when :ios
|
|
247
|
+
# For iOS, we look for 'apple_api_key'
|
|
248
|
+
config.dig('fastlane', 'apple_api_key')
|
|
249
|
+
else
|
|
250
|
+
# Return nil for other platforms
|
|
251
|
+
nil
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# @return [String, nil] The export_option_file.
|
|
256
|
+
def get_export_option_file(platform, flavor_name)
|
|
257
|
+
# 1. Get the merged configuration for the specific flavor
|
|
258
|
+
config = get_merged_flavor_config(platform, flavor_name)
|
|
259
|
+
|
|
260
|
+
# Return nil early if configuration is missing
|
|
261
|
+
return nil unless config
|
|
262
|
+
|
|
263
|
+
# 2. Extract the key based on the platform
|
|
264
|
+
case platform
|
|
265
|
+
when :ios
|
|
266
|
+
# For iOS, we look for 'export_options_plist'
|
|
267
|
+
config.dig('fastlane', 'export_options_plist')
|
|
268
|
+
else
|
|
269
|
+
# Return nil for other platforms
|
|
270
|
+
nil
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# @return [String, nil] The App store Team ID.
|
|
275
|
+
def get_team_id(platform, flavor_name)
|
|
276
|
+
# 1. Get the merged configuration for the specific flavor
|
|
277
|
+
config = get_merged_flavor_config(platform, flavor_name)
|
|
278
|
+
|
|
279
|
+
# Return nil early if configuration is missing
|
|
280
|
+
return nil unless config
|
|
281
|
+
|
|
282
|
+
# 2. Extract the key based on the platform
|
|
283
|
+
case platform
|
|
284
|
+
when :ios
|
|
285
|
+
# For iOS, we look for 'export_option_file'
|
|
286
|
+
config.dig('team_id')
|
|
287
|
+
else
|
|
288
|
+
# Return nil for other platforms
|
|
289
|
+
nil
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# @return [String, nil] The Firebase Project ID.
|
|
294
|
+
def get_firebase_project_id(platform, flavor_name, build_config)
|
|
295
|
+
# 1. Get the merged configuration for the specific flavor
|
|
296
|
+
config = get_merged_flavor_config(platform, flavor_name)
|
|
297
|
+
|
|
298
|
+
config.dig('firebase', build_config, 'project_id') if config
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Retrieves the firebase token from a external .properties file
|
|
302
|
+
# as defined in annai_app -> general -> firebase_token_file
|
|
303
|
+
# @return [String, nil] The token value, or nil if not found/error.
|
|
304
|
+
def get_firebase_token_from_properties
|
|
305
|
+
return nil unless @spec_data
|
|
306
|
+
|
|
307
|
+
# 1. Get the properties file path from the YAML spec
|
|
308
|
+
# Path: annai_app -> general -> firebase_token_file
|
|
309
|
+
rel_props_path = @spec_data.dig('annai_app', 'general', 'firebase_token_file')
|
|
310
|
+
|
|
311
|
+
if rel_props_path.nil? || rel_props_path.empty?
|
|
312
|
+
Fastlane::UI.verbose("No 'firebase_token_file' defined in annai_app -> general.")
|
|
313
|
+
return nil
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# 2. Resolve the full path relative to project root
|
|
317
|
+
full_props_path = File.expand_path(rel_props_path, @root_folder)
|
|
318
|
+
|
|
319
|
+
unless File.exist?(full_props_path)
|
|
320
|
+
Fastlane::UI.error("Firebase token file not found at: #{full_props_path}")
|
|
321
|
+
return nil
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# 3. Parse the .properties file for FIREBASE_TOKEN
|
|
325
|
+
begin
|
|
326
|
+
File.foreach(full_props_path) do |line|
|
|
327
|
+
# Remove whitespace and skip comments/empty lines
|
|
328
|
+
line = line.strip
|
|
329
|
+
next if line.empty? || line.start_with?('#', '!')
|
|
330
|
+
|
|
331
|
+
# Match KEY=VALUE (handles both = and : as separators)
|
|
332
|
+
if line =~ /^\s*FIREBASE_TOKEN\s*[=:]\s*(.*)$/
|
|
333
|
+
token = $1.strip
|
|
334
|
+
# Remove optional surrounding quotes if present
|
|
335
|
+
return token.gsub(/^['"]|['"]$/, '')
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
rescue => e
|
|
339
|
+
Fastlane::UI.error("Error reading properties file at #{rel_props_path}: #{e.message}")
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
Fastlane::UI.verbose("FIREBASE_TOKEN key not found in #{rel_props_path}")
|
|
343
|
+
return nil
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# For AdMob (annai_ads)
|
|
347
|
+
def get_gms_ads_id(platform, flavor_name)
|
|
348
|
+
config = get_merged_flavor_config(platform, flavor_name)
|
|
349
|
+
config.dig('gms_ads_id') if config
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# For Google Sign-In (annai_auth)
|
|
353
|
+
def get_auth_client_id(platform, flavor_name, build_config)
|
|
354
|
+
config = get_merged_flavor_config(platform, flavor_name)
|
|
355
|
+
config.dig('auth', build_config, 'clientId') if config
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def get_auth_reversed_client_id(platform, flavor_name, build_config)
|
|
359
|
+
config = get_merged_flavor_config(platform, flavor_name)
|
|
360
|
+
config.dig('auth', build_config, 'reversedClientId') if config
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
require 'yaml'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
require 'fastlane/action'
|
|
4
|
+
|
|
5
|
+
module FastlaneFlutterFlavor
|
|
6
|
+
|
|
7
|
+
# Inject the custom colors used in the StatusManager (since they aren't standard UI functions)
|
|
8
|
+
def red(text) ; "\e[31m#{text}\e[0m" ; end
|
|
9
|
+
def green(text) ; "\e[32m#{text}\e[0m" ; end
|
|
10
|
+
def amber(text) ; "\e[33m#{text}\e[0m" ; end
|
|
11
|
+
|
|
12
|
+
# ------------------------------------
|
|
13
|
+
# StatusManager (Helper Class)
|
|
14
|
+
# ------------------------------------
|
|
15
|
+
class StatusManager
|
|
16
|
+
|
|
17
|
+
def initialize(lane:)
|
|
18
|
+
@status = Hash.new
|
|
19
|
+
@lane = lane
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def red(text) ; "\e[31m#{text}\e[0m" ; end
|
|
23
|
+
def green(text) ; "\e[32m#{text}\e[0m" ; end
|
|
24
|
+
def amber(text) ; "\e[33m#{text}\e[0m" ; end
|
|
25
|
+
|
|
26
|
+
def logError(flavor, laneName, variant, message)
|
|
27
|
+
key = [flavor, laneName, variant]
|
|
28
|
+
|
|
29
|
+
errorPresent = false
|
|
30
|
+
if @status.key?(key)
|
|
31
|
+
errorPresent = @status[key][0] == "err"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
if errorPresent
|
|
35
|
+
message = "#{@status[key][1]}\n#{message}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
@status[key] = ["err", message]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def logWarning(flavor, laneName, variant, message)
|
|
42
|
+
key = [flavor, laneName, variant]
|
|
43
|
+
|
|
44
|
+
errorPresent = false
|
|
45
|
+
if @status.key?(key)
|
|
46
|
+
errorPresent = @status[key][0] == "err"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
unless errorPresent
|
|
50
|
+
@status[key] = ["warn", message]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def logSuccess(flavor, laneName, variant)
|
|
55
|
+
key = [flavor, laneName, variant]
|
|
56
|
+
|
|
57
|
+
errorPresent = false
|
|
58
|
+
if @status.key?(key)
|
|
59
|
+
errorPresent = @status[key][0] == "err" || @status[key][0] == "warn"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
unless errorPresent
|
|
63
|
+
@status[key] = ["info", ""]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def displayStatus()
|
|
68
|
+
errMsg = ""
|
|
69
|
+
message = "\n"
|
|
70
|
+
message += "+-------------------+-------------------------------+-------------------------------+----------------+\n"
|
|
71
|
+
message += "| #{green('Annai Fastlane summary')} |\n"
|
|
72
|
+
message += "+-------------------+-------------------------------+-------------------------------+----------------+\n"
|
|
73
|
+
|
|
74
|
+
message += "| Flavor | Variant | Lane | Status |\n"
|
|
75
|
+
message += "+-------------------+-------------------------------+-------------------------------+----------------+\n"
|
|
76
|
+
|
|
77
|
+
@status.each { |key,value|
|
|
78
|
+
flavor = key[0].to_s
|
|
79
|
+
laneName = key[1].to_s
|
|
80
|
+
variantName = key[2].to_s
|
|
81
|
+
statusStr = value[0]
|
|
82
|
+
statusMessage = value[1]
|
|
83
|
+
|
|
84
|
+
if statusStr == 'info'
|
|
85
|
+
sts = green("Success".ljust(15))
|
|
86
|
+
elsif statusStr == 'warn'
|
|
87
|
+
sts = amber("Warning".ljust(15))
|
|
88
|
+
if errMsg != "" then
|
|
89
|
+
errMsg += "\n"
|
|
90
|
+
end
|
|
91
|
+
errMsg += "Flavor - #{flavor} : "
|
|
92
|
+
errMsg += "#{statusMessage}"
|
|
93
|
+
else
|
|
94
|
+
sts = red("Failure".ljust(15))
|
|
95
|
+
if errMsg != "" then
|
|
96
|
+
errMsg += "\n"
|
|
97
|
+
end
|
|
98
|
+
errMsg += "Flavor - #{flavor} : "
|
|
99
|
+
errMsg += "#{statusMessage}"
|
|
100
|
+
end
|
|
101
|
+
message += "| #{flavor.ljust(18)}| #{variantName.ljust(30)}| #{laneName.ljust(30)}| #{sts}|\n"
|
|
102
|
+
}
|
|
103
|
+
message += "+-------------------+-------------------------------+-------------------------------+----------------+"
|
|
104
|
+
|
|
105
|
+
if errMsg != "" then
|
|
106
|
+
Fastlane::UI.message("Summary of all the Errors/Warnings")
|
|
107
|
+
Fastlane::UI.error(errMsg)
|
|
108
|
+
# Mailgun calls removed, assuming external communication is handled outside the core action
|
|
109
|
+
else
|
|
110
|
+
# Mailgun calls removed
|
|
111
|
+
end
|
|
112
|
+
Fastlane::UI.message(message)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Fastlane
|
|
2
|
+
module AnnFlutterFlavor
|
|
3
|
+
def self.all_classes
|
|
4
|
+
Dir[File.expand_path('**/{actions,helper}/*.rb', File.dirname(__FILE__))]
|
|
5
|
+
end
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Import all available actions and helpers
|
|
10
|
+
Fastlane::AnnFlutterFlavor.all_classes.each { |f| require f }
|
|
11
|
+
|
|
12
|
+
# --- Global Helper for CocoaPods Podfile ---
|
|
13
|
+
|
|
14
|
+
# Accessible directly in the Podfile's post_install block.
|
|
15
|
+
def annai_ios_podfile_setup(installer)
|
|
16
|
+
if defined?(FastlaneFlutterFlavor::PodspecBridge)
|
|
17
|
+
FastlaneFlutterFlavor::PodspecBridge.apply_build_settings(installer)
|
|
18
|
+
else
|
|
19
|
+
# Fallback if Fastlane UI isn't loaded
|
|
20
|
+
msg = "[Annai] Bridge not found. Check gem installation."
|
|
21
|
+
defined?(UI) ? UI.error(msg) : puts("\e[31m#{msg}\e[0m")
|
|
22
|
+
end
|
|
23
|
+
end
|