fastlane-plugin-wpmreleasetoolkit 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +339 -0
- data/README.md +38 -0
- data/bin/drawText +19 -0
- data/ext/drawText/extconf.rb +36 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit.rb +16 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/README.md +20 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/an_localize_libs_action.rb +53 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/an_update_metadata_source_action.rb +171 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/an_validate_lib_strings_action.rb +63 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_betabuild_prechecks.rb +103 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_build_prechecks.rb +83 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_build_preflight.rb +54 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_bump_version_beta.rb +69 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_bump_version_final_release.rb +58 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_bump_version_hotfix.rb +77 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_bump_version_release.rb +89 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_codefreeze_prechecks.rb +79 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_completecodefreeze_prechecks.rb +68 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_create_xml_release_notes.rb +63 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_current_branch_is_hotfix.rb +44 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_download_file_by_version.rb +79 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_download_translations_action.rb +115 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_finalize_prechecks.rb +71 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_get_alpha_version.rb +44 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_get_app_version.rb +44 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_get_release_version.rb +44 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_hotifx_prechecks.rb +78 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_merge_translators_strings.rb +106 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_tag_build.rb +51 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_update_metadata.rb +52 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_update_release_notes.rb +56 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/circleci_trigger_job_action.rb +63 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/close_milestone_action.rb +56 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_new_milestone_action.rb +59 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_release_action.rb +91 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/extract_release_notes_for_version_action.rb +89 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/get_prs_list_action.rb +64 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/gp_downloadmetadata_action.rb +90 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/gp_update_metadata_source.rb +170 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/promo_screenshots_action.rb +247 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/removebranchprotection_action.rb +57 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/setbranchprotection_action.rb +56 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/setfrozentag_action.rb +81 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_add_files_to_copy_action.rb +96 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_apply_action.rb +139 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_download_action.rb +57 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_setup_action.rb +86 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_update_action.rb +139 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_validate_action.rb +134 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/add_development_certificates_to_provisioning_profiles.rb +77 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/add_devices_to_provisioning_profiles.rb +79 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_betabuild_prechecks.rb +92 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_build_prechecks.rb +74 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_build_preflight.rb +78 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_bump_version_beta.rb +68 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_bump_version_hotfix.rb +87 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_bump_version_release.rb +114 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_check_beta_deps.rb +62 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_clear_intermediate_tags.rb +60 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_codefreeze_prechecks.rb +70 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_completecodefreeze_prechecks.rb +63 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_current_branch_is_hotfix.rb +40 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_final_tag.rb +52 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_finalize_prechecks.rb +64 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_get_app_version.rb +47 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_get_build_version.rb +60 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_get_store_app_sizes.rb +121 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_hotifx_prechecks.rb +78 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_lint_localizations.rb +167 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_localize_project.rb +44 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_merge_translators_strings.rb +93 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_tag_build.rb +44 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_update_metadata.rb +40 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_update_metadata_source.rb +81 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_update_release_notes.rb +56 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_validate_ci_build.rb +46 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/an_metadata_update_helper.rb +152 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_git_helper.rb +44 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb +359 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_version_helper.rb +475 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ci_helper.rb +91 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/configure_helper.rb +282 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/encryption_helper.rb +51 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/filesystem_helper.rb +93 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/git_helper.rb +224 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb +135 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_adc_app_sizes_helper.rb +74 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_git_helper.rb +80 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_helper.rb +208 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_version_helper.rb +348 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/metadata_download_helper.rb +107 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/metadata_update_helper.rb +182 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/promo_screenshots_helper.rb +399 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/release_notes_helper.rb +21 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/models/configuration.rb +40 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/models/file_reference.rb +86 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/version.rb +5 -0
- metadata +449 -0
@@ -0,0 +1,359 @@
|
|
1
|
+
require 'fastlane_core/ui/ui'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'nokogiri'
|
4
|
+
require 'open-uri'
|
5
|
+
|
6
|
+
module Fastlane
|
7
|
+
UI = FastlaneCore::UI unless Fastlane.const_defined?('UI')
|
8
|
+
|
9
|
+
module Helper
|
10
|
+
module Android
|
11
|
+
module LocalizeHelper
|
12
|
+
# Checks if string_line has the content_override flag set
|
13
|
+
def self.skip_string_by_tag(string_line)
|
14
|
+
skip = string_line.attr('content_override') == 'true' unless string_line.attr('content_override').nil?
|
15
|
+
if skip
|
16
|
+
puts " - Skipping #{string_line.attr('name')} string"
|
17
|
+
return true
|
18
|
+
end
|
19
|
+
|
20
|
+
return false
|
21
|
+
end
|
22
|
+
|
23
|
+
# Checks if string_name is in the excluesion list
|
24
|
+
def self.skip_string_by_exclusion_list(library, string_name)
|
25
|
+
return false unless library.key?(:exclusions)
|
26
|
+
|
27
|
+
skip = library[:exclusions].include?(string_name)
|
28
|
+
if skip
|
29
|
+
puts " - Skipping #{string_name} string"
|
30
|
+
return true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Merge string_line into main_string
|
35
|
+
def self.merge_string(main_strings, library, string_line)
|
36
|
+
string_name = string_line.attr('name')
|
37
|
+
string_content = string_line.content
|
38
|
+
|
39
|
+
# Skip strings in the exclusions list
|
40
|
+
return :skipped if skip_string_by_exclusion_list(library, string_name)
|
41
|
+
|
42
|
+
# Search for the string in the main file
|
43
|
+
result = :added
|
44
|
+
main_strings.xpath('//string').each do |this_string|
|
45
|
+
if this_string.attr('name') == string_name
|
46
|
+
# Skip if the string has the content_override tag
|
47
|
+
return :skipped if skip_string_by_tag(this_string)
|
48
|
+
|
49
|
+
# If nodes are equivalent, skip
|
50
|
+
return :found if string_line =~ this_string
|
51
|
+
|
52
|
+
# The string needs an update
|
53
|
+
result = :updated
|
54
|
+
if this_string.attr('tools:ignore').nil?
|
55
|
+
# It can be updated, so remove the current one and move ahead
|
56
|
+
this_string.remove
|
57
|
+
break
|
58
|
+
else
|
59
|
+
# It has the tools:ignore flag, so update the content without touching the other attributes
|
60
|
+
this_string.content = string_content
|
61
|
+
return result
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# String not found, or removed because needing update and not in the exclusion list: add to the main file
|
67
|
+
main_strings.xpath('//string').last().add_next_sibling("\n#{' ' * 4}#{string_line.to_xml().strip}")
|
68
|
+
return result
|
69
|
+
end
|
70
|
+
|
71
|
+
# Verify a string
|
72
|
+
def self.verify_string(main_strings, library, string_line)
|
73
|
+
string_name = string_line.attr('name')
|
74
|
+
string_content = string_line.content
|
75
|
+
|
76
|
+
# Skip strings in the exclusions list
|
77
|
+
return if skip_string_by_exclusion_list(library, string_name)
|
78
|
+
|
79
|
+
# Search for the string in the main file
|
80
|
+
main_strings.xpath('//string').each do |this_string|
|
81
|
+
if this_string.attr('name') == string_name
|
82
|
+
# Skip if the string has the content_override tag
|
83
|
+
return if skip_string_by_tag(this_string)
|
84
|
+
|
85
|
+
# Update if needed
|
86
|
+
UI.user_error!("String #{string_name} [#{string_content}] has been updated in the main file but not in the library #{library[:library]}.") if this_string.content != string_content
|
87
|
+
return
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# String not found and not in the exclusion list:
|
92
|
+
UI.user_error!("String #{string_name} [#{string_content}] was found in library #{library[:library]} but not in the main file.")
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.merge_lib(main, library)
|
96
|
+
UI.message("Merging #{library[:library]} strings into #{main}")
|
97
|
+
main_strings = File.open(main) { |f| Nokogiri::XML(f, nil, Encoding::UTF_8.to_s) }
|
98
|
+
lib_strings = File.open(library[:strings_path]) { |f| Nokogiri::XML(f, nil, Encoding::UTF_8.to_s) }
|
99
|
+
|
100
|
+
updated_count = 0
|
101
|
+
untouched_count = 0
|
102
|
+
added_count = 0
|
103
|
+
skipped_count = 0
|
104
|
+
lib_strings.xpath('//string').each do |string_line|
|
105
|
+
res = merge_string(main_strings, library, string_line)
|
106
|
+
case res
|
107
|
+
when :updated
|
108
|
+
puts "#{string_line.attr('name')} updated."
|
109
|
+
updated_count = updated_count + 1
|
110
|
+
when :found
|
111
|
+
untouched_count = untouched_count + 1
|
112
|
+
when :added
|
113
|
+
puts "#{string_line.attr('name')} added."
|
114
|
+
added_count = added_count + 1
|
115
|
+
when :skipped
|
116
|
+
skipped_count = skipped_count + 1
|
117
|
+
else
|
118
|
+
UI.user_error!("Internal Error! #{res}")
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
File.open(main, 'w:UTF-8') do |f|
|
123
|
+
f.write(main_strings.to_xml(indent: 4))
|
124
|
+
end
|
125
|
+
|
126
|
+
UI.message("Done (#{added_count} added, #{updated_count} updated, #{untouched_count} untouched, #{skipped_count} skipped).")
|
127
|
+
return (added_count + updated_count) != 0
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.verify_diff(diff_string, main_strings, lib_strings, library)
|
131
|
+
if diff_string.start_with?('name=')
|
132
|
+
diff_string.slice!('name="')
|
133
|
+
|
134
|
+
end_index = diff_string.index('"')
|
135
|
+
end_index ||= diff_string.length # Use the whole string if there's no '"'
|
136
|
+
|
137
|
+
diff_string = diff_string.slice(0..(end_index - 1))
|
138
|
+
|
139
|
+
lib_strings.xpath('//string').each do |string_line|
|
140
|
+
res = verify_string(main_strings, library, string_line) if string_line.attr('name') == diff_string
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def self.verify_lib(main, library, source_diff)
|
146
|
+
UI.message("Checking #{library[:library]} strings vs #{main}")
|
147
|
+
main_strings = File.open(main) { |f| Nokogiri::XML(f, nil, Encoding::UTF_8.to_s) }
|
148
|
+
lib_strings = File.open(library[:strings_path]) { |f| Nokogiri::XML(f, nil, Encoding::UTF_8.to_s) }
|
149
|
+
|
150
|
+
verify_local_diff(main, library, main_strings, lib_strings)
|
151
|
+
verify_pr_diff(main, library, main_strings, lib_strings, source_diff) unless source_diff.nil?
|
152
|
+
end
|
153
|
+
|
154
|
+
def self.verify_local_diff(main, library, main_strings, lib_strings)
|
155
|
+
`git diff #{main}`.each_line do |line|
|
156
|
+
if line.start_with?('+ ') || line.start_with?('- ')
|
157
|
+
diffs = line.gsub(/\s+/m, ' ').strip.split(' ')
|
158
|
+
diffs.each do |diff|
|
159
|
+
verify_diff(diff, main_strings, lib_strings, library)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def self.verify_pr_diff(main, library, main_strings, lib_strings, source_diff)
|
166
|
+
source_diff.each_line do |line|
|
167
|
+
if line.start_with?('+ ') || line.start_with?('- ')
|
168
|
+
diffs = line.gsub(/\s+/m, ' ').strip.split(' ')
|
169
|
+
diffs.each do |diff|
|
170
|
+
verify_diff(diff, main_strings, lib_strings, library)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
########
|
177
|
+
# @!group Downloading translations from GlotPress
|
178
|
+
########
|
179
|
+
|
180
|
+
# Create the `available_languages.xml` file.
|
181
|
+
#
|
182
|
+
# @param [String] res_dir The relative path to the `…/src/main/res` directory
|
183
|
+
# @param [Array<String>] locale_codes The list of locale codes to include in the available_languages.xml file
|
184
|
+
#
|
185
|
+
def self.create_available_languages_file(res_dir:, locale_codes:)
|
186
|
+
doc = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
|
187
|
+
xml.comment('Warning: Auto-generated file, do not edit.')
|
188
|
+
xml.resources do
|
189
|
+
xml.send(:'string-array', name: 'available_languages', translatable: 'false') do
|
190
|
+
locale_codes.each { |code| xml.item(code.gsub('-r', '_')) }
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
File.write(File.join(res_dir, 'values', 'available_languages.xml'), doc.to_xml)
|
195
|
+
end
|
196
|
+
|
197
|
+
# Download translations from GlotPress
|
198
|
+
#
|
199
|
+
# @param [String] res_dir The relative path to the `…/src/main/res` directory.
|
200
|
+
# @param [String] glotpress_project_url The base URL to the glotpress project to download the strings from.
|
201
|
+
# @param [Hash{String=>String}, Array] glotpress_filters
|
202
|
+
# The filters to apply when exporting strings from GlotPress.
|
203
|
+
# Typical examples include `{ status: 'current' }` or `{ status: 'review' }`.
|
204
|
+
# If an array of Hashes is provided instead of a single Hash, this method will perform as many
|
205
|
+
# export requests as items in this array, then merge all the results – useful for OR-ing multiple filters.
|
206
|
+
# @param [Array<Hash{Symbol=>String}>] locales_map
|
207
|
+
# An array of locales to download. Each item in the array must be a Hash
|
208
|
+
# with keys `:glotpress` and `:android` containing the respective locale codes.
|
209
|
+
#
|
210
|
+
def self.download_from_glotpress(res_dir:, glotpress_project_url:, glotpress_filters: { status: 'current' }, locales_map:)
|
211
|
+
glotpress_filters = [glotpress_filters] unless glotpress_filters.is_a?(Array)
|
212
|
+
|
213
|
+
attributes_to_copy = %w[formatted] # Attributes that we want to replicate into translated `string.xml` files
|
214
|
+
orig_file = File.join(res_dir, 'values', 'strings.xml')
|
215
|
+
orig_xml = File.open(orig_file) { |f| Nokogiri::XML(f, nil, Encoding::UTF_8.to_s) }
|
216
|
+
orig_attributes = orig_xml.xpath('//string').map { |tag| [tag['name'], tag.attributes.select { |k, _| attributes_to_copy.include?(k) }] }.to_h
|
217
|
+
|
218
|
+
locales_map.each do |lang_codes|
|
219
|
+
all_xml_documents = glotpress_filters.map do |filters|
|
220
|
+
UI.message "Downloading translations for '#{lang_codes[:android]}' from GlotPress (#{lang_codes[:glotpress]}) [#{filters}]..."
|
221
|
+
download_glotpress_export_file(project_url: glotpress_project_url, locale: lang_codes[:glotpress], filters: filters)
|
222
|
+
end.compact
|
223
|
+
next if all_xml_documents.empty?
|
224
|
+
|
225
|
+
# Merge all XMLs together
|
226
|
+
merged_xml = merge_xml_documents(all_xml_documents)
|
227
|
+
|
228
|
+
# Process XML (text substitutions, replicate attributes, quick-lint string)
|
229
|
+
merged_xml.xpath('//string').each do |string_tag|
|
230
|
+
apply_substitutions(string_tag)
|
231
|
+
orig_attributes[string_tag['name']]&.each { |k, v| string_tag[k] = v }
|
232
|
+
quick_lint(string_tag, lang_codes[:android])
|
233
|
+
end
|
234
|
+
merged_xml.xpath('//string-array/item').each { |item_tag| apply_substitutions(item_tag) }
|
235
|
+
|
236
|
+
# Save
|
237
|
+
lang_dir = File.join(res_dir, "values-#{lang_codes[:android]}")
|
238
|
+
FileUtils.mkdir(lang_dir) unless Dir.exist?(lang_dir)
|
239
|
+
lang_file = File.join(lang_dir, 'strings.xml')
|
240
|
+
File.open(lang_file, 'w') { |f| merged_xml.write_to(f, encoding: Encoding::UTF_8.to_s, indent: 4) }
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
#####################
|
245
|
+
# Private Helpers
|
246
|
+
#####################
|
247
|
+
|
248
|
+
# Downloads the export from GlotPress for a given locale and given filters
|
249
|
+
#
|
250
|
+
# @param [String] project_url The URL to the GlotPress project to export from.
|
251
|
+
# @param [String] locale The GlotPress locale code to download strings for.
|
252
|
+
# @param [Hash{Symbol=>String}] filters The hash of filters to apply when exporting from GlotPress.
|
253
|
+
# Typical examples include `{ status: 'current' }` or `{ status: 'review' }`.
|
254
|
+
# @return [Nokogiri::XML] the download XML document, parsed as a Nokogiri::XML object
|
255
|
+
#
|
256
|
+
def self.download_glotpress_export_file(project_url:, locale:, filters:)
|
257
|
+
query_params = filters.transform_keys { |k| "filters[#{k}]" }.merge(format: 'android')
|
258
|
+
uri = URI.parse("#{project_url.chomp('/')}/#{locale}/default/export-translations?#{URI.encode_www_form(query_params)}")
|
259
|
+
begin
|
260
|
+
uri.open { |f| Nokogiri::XML(f.read.gsub("\t", ' '), nil, Encoding::UTF_8.to_s) }
|
261
|
+
rescue StandardError => e
|
262
|
+
UI.error "Error downloading #{locale} - #{e.message}"
|
263
|
+
return nil
|
264
|
+
end
|
265
|
+
end
|
266
|
+
private_class_method :download_glotpress_export_file
|
267
|
+
|
268
|
+
# Merge multiple Nokogiri::XML `strings.xml` documents together
|
269
|
+
#
|
270
|
+
# @param [Array<Nokogiri::XML::Document>] all_xmls Array of the Nokogiri XML documents to merge together
|
271
|
+
# @return [Nokogiri::XML::Document] The merged document.
|
272
|
+
#
|
273
|
+
# @note The first document in the array is used as starting point. Then string/resources from other docs are merged into it.
|
274
|
+
# If a string/resource with a given `name` is present in multiple documents, the node of the last one wins.
|
275
|
+
#
|
276
|
+
def self.merge_xml_documents(all_xmls)
|
277
|
+
return all_xmls.first if all_xmls.count <= 1
|
278
|
+
|
279
|
+
merged_xml = all_xmls.first.dup # Use the first XML as starting point
|
280
|
+
resources_node = merged_xml.xpath('/resources').first
|
281
|
+
# For each other XML, find all the nodes with a name attribute, and merge them in
|
282
|
+
all_xmls.drop(1).each do |other_xml|
|
283
|
+
other_xml.xpath('/resources/*[@name]').each do |other_node|
|
284
|
+
existing_node = merged_xml.xpath("//#{other_node.name}[@name='#{other_node['name']}']").first
|
285
|
+
if existing_node.nil?
|
286
|
+
resources_node << ' ' << other_node << "\n"
|
287
|
+
else
|
288
|
+
existing_node.replace(other_node)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
merged_xml
|
293
|
+
end
|
294
|
+
private_class_method :merge_xml_documents
|
295
|
+
|
296
|
+
# Apply some common text substitutions to tag contents
|
297
|
+
#
|
298
|
+
# @param [Nokogiri::XML::Node] tag The XML tag/node to apply substitutions to
|
299
|
+
#
|
300
|
+
def self.apply_substitutions(tag)
|
301
|
+
tag.content = tag.content.gsub('...', '…')
|
302
|
+
end
|
303
|
+
private_class_method :apply_substitutions
|
304
|
+
|
305
|
+
# Perform some quick basic checks about an individual `<string>` tag and print warnings accordingly
|
306
|
+
#
|
307
|
+
# @param [Nokogiri::XML::Node] string_tag The XML tag/node to check
|
308
|
+
# @param [String] lang The language we are currently processing. Used for providing context during logging / warning message
|
309
|
+
#
|
310
|
+
def self.quick_lint(string_tag, lang)
|
311
|
+
if string_tag['formatted'] == 'false' && string_tag.content.include?('%%')
|
312
|
+
UI.important "Warning: [#{lang}] translation for '#{string_tag['name']}' has attribute formatted=false, but still contains escaped '%%' in translation."
|
313
|
+
end
|
314
|
+
end
|
315
|
+
private_class_method :quick_lint
|
316
|
+
|
317
|
+
# @!endgroup
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
# Source: https://stackoverflow.com/questions/7825258/determine-if-two-nokogiri-nodes-are-equivalent?rq=1
|
324
|
+
# There may be better solutions now that Ruby supports canonicalization.
|
325
|
+
module Nokogiri
|
326
|
+
module XML
|
327
|
+
class Node
|
328
|
+
# Return true if this node is content-equivalent to other, false otherwise
|
329
|
+
def =~(other)
|
330
|
+
return true if self == other
|
331
|
+
return false unless name == other.name
|
332
|
+
|
333
|
+
stype = node_type
|
334
|
+
otype = other.node_type
|
335
|
+
return false unless stype == otype
|
336
|
+
|
337
|
+
sa = attributes
|
338
|
+
oa = other.attributes
|
339
|
+
return false unless sa.length == oa.length
|
340
|
+
|
341
|
+
sa = sa.sort.map { |n, a| [n, a.value, a.namespace && a.namespace.href] }
|
342
|
+
oa = oa.sort.map { |n, a| [n, a.value, a.namespace && a.namespace.href] }
|
343
|
+
return false unless sa == oa
|
344
|
+
|
345
|
+
skids = children
|
346
|
+
okids = other.children
|
347
|
+
return false unless skids.length == okids.length
|
348
|
+
return false if stype == TEXT_NODE && (content != other.content)
|
349
|
+
|
350
|
+
sns = namespace
|
351
|
+
ons = other.namespace
|
352
|
+
return false if !sns ^ !ons
|
353
|
+
return false if sns && (sns.href != ons.href)
|
354
|
+
|
355
|
+
skids.to_enum.with_index.all? { |ski, i| ski =~ okids[i] }
|
356
|
+
end
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|
@@ -0,0 +1,475 @@
|
|
1
|
+
module Fastlane
|
2
|
+
module Helper
|
3
|
+
module Android
|
4
|
+
# A module containing helper methods to manipulate/extract/bump Android version strings in gradle files
|
5
|
+
#
|
6
|
+
module VersionHelper
|
7
|
+
# The key used in internal version Hash objects to hold the versionName value
|
8
|
+
VERSION_NAME = 'name'
|
9
|
+
# The key used in internal version Hash objects to hold the versionCode value
|
10
|
+
VERSION_CODE = 'code'
|
11
|
+
# The index for the major version number part
|
12
|
+
MAJOR_NUMBER = 0
|
13
|
+
# The index for the minor version number part
|
14
|
+
MINOR_NUMBER = 1
|
15
|
+
# The index for the hotfix version number part
|
16
|
+
HOTFIX_NUMBER = 2
|
17
|
+
# The prefix used in front of the versionName for alpha versions
|
18
|
+
ALPHA_PREFIX = 'alpha-'
|
19
|
+
# The suffix used in the versionName for RC (beta) versions
|
20
|
+
RC_SUFFIX = '-rc'
|
21
|
+
|
22
|
+
# Returns the public-facing version string.
|
23
|
+
#
|
24
|
+
# @example
|
25
|
+
# "1.2" # Assuming build.gradle contains versionName "1.2"
|
26
|
+
# "1.2" # Assuming build.gradle contains versionName "1.2.0"
|
27
|
+
# "1.2.3" # Assuming build.gradle contains versionName "1.2.3"
|
28
|
+
#
|
29
|
+
# @return [String] The public-facing version number, extracted from the `versionName` of the `build.gradle` file.
|
30
|
+
# - If this version is a hotfix (more than 2 parts and 3rd part is non-zero), returns the "X.Y.Z" formatted string
|
31
|
+
# - Otherwise (not a hotfix / 3rd part of version is 0), returns "X.Y" formatted version number
|
32
|
+
#
|
33
|
+
def self.get_public_version
|
34
|
+
version = get_release_version
|
35
|
+
vp = get_version_parts(version[VERSION_NAME])
|
36
|
+
return "#{vp[MAJOR_NUMBER]}.#{vp[MINOR_NUMBER]}" unless is_hotfix?(version)
|
37
|
+
|
38
|
+
"#{vp[MAJOR_NUMBER]}.#{vp[MINOR_NUMBER]}.#{vp[HOTFIX_NUMBER]}"
|
39
|
+
end
|
40
|
+
|
41
|
+
# Extract the version name and code from the `vanilla` flavor of the `$PROJECT_NAME/build.gradle file`
|
42
|
+
# or for the defaultConfig if `HAS_ALPHA_VERSION` is not defined.
|
43
|
+
#
|
44
|
+
# @env HAS_ALPHA_VERSION If set (with any value), indicates that the project uses `vanilla` flavor.
|
45
|
+
#
|
46
|
+
# @return [Hash] A hash with 2 keys "name" and "code" containing the extracted version name and code, respectively
|
47
|
+
#
|
48
|
+
def self.get_release_version
|
49
|
+
section = ENV['HAS_ALPHA_VERSION'].nil? ? 'defaultConfig' : 'vanilla {'
|
50
|
+
gradle_path = self.gradle_path
|
51
|
+
name = get_version_name_from_gradle_file(gradle_path, section)
|
52
|
+
code = get_version_build_from_gradle_file(gradle_path, section)
|
53
|
+
return { VERSION_NAME => name, VERSION_CODE => code }
|
54
|
+
end
|
55
|
+
|
56
|
+
# Extract the version name and code from the `defaultConfig` of the `$PROJECT_NAME/build.gradle` file
|
57
|
+
#
|
58
|
+
# @return [Hash] A hash with 2 keys `"name"` and `"code"` containing the extracted version name and code, respectively,
|
59
|
+
# or `nil` if `$HAS_ALPHA_VERSION` is not defined.
|
60
|
+
#
|
61
|
+
def self.get_alpha_version
|
62
|
+
return nil if ENV['HAS_ALPHA_VERSION'].nil?
|
63
|
+
|
64
|
+
section = 'defaultConfig'
|
65
|
+
gradle_path = self.gradle_path
|
66
|
+
name = get_version_name_from_gradle_file(gradle_path, section)
|
67
|
+
code = get_version_build_from_gradle_file(gradle_path, section)
|
68
|
+
return { VERSION_NAME => name, VERSION_CODE => code }
|
69
|
+
end
|
70
|
+
|
71
|
+
# Determines if a version name corresponds to an alpha version (starts with `"alpha-"`` prefix)
|
72
|
+
#
|
73
|
+
# @param [String] version The version name to check
|
74
|
+
#
|
75
|
+
# @return [Bool] true if the version name starts with the `ALPHA_PREFIX`, false otherwise.
|
76
|
+
#
|
77
|
+
# @private
|
78
|
+
#
|
79
|
+
def self.is_alpha_version?(version)
|
80
|
+
version[VERSION_NAME].start_with?(ALPHA_PREFIX)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Check if this versionName corresponds to a beta, i.e. contains some `-rc` suffix
|
84
|
+
#
|
85
|
+
# @param [String] version The versionName string to check for
|
86
|
+
#
|
87
|
+
# @return [Bool] True if the version string contains `-rc`, indicating it is a beta version.
|
88
|
+
#
|
89
|
+
def self.is_beta_version?(version)
|
90
|
+
version[VERSION_NAME].include?(RC_SUFFIX)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Returns the version name and code to use for the final release.
|
94
|
+
#
|
95
|
+
# - The final version name corresponds to the beta's versionName, without the `-rc` suffix
|
96
|
+
# - The final version code corresponds to the versionCode for the alpha (or for the beta if alpha_version is nil) incremented by one.
|
97
|
+
#
|
98
|
+
# @param [Hash] beta_version The version hash for the beta (vanilla flavor), containing values for keys "name" and "code"
|
99
|
+
# @param [Hash] alpha_version The version hash for the alpha (defaultConfig), containing values for keys "name" and "code",
|
100
|
+
# or `nil` if no alpha version to consider.
|
101
|
+
#
|
102
|
+
# @return [Hash] A version hash with keys "name" and "code", containing the version name and code to use for final release.
|
103
|
+
#
|
104
|
+
def self.calc_final_release_version(beta_version, alpha_version)
|
105
|
+
version_name = beta_version[VERSION_NAME].split('-')[0]
|
106
|
+
version_code = alpha_version.nil? ? beta_version[VERSION_CODE] + 1 : alpha_version[VERSION_CODE] + 1
|
107
|
+
|
108
|
+
{ VERSION_NAME => version_name, VERSION_CODE => version_code }
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns the version name and code to use for the next alpha.
|
112
|
+
#
|
113
|
+
# - The next version name corresponds to the `alpha_version`'s name incremented by one (alpha-42 => alpha-43)
|
114
|
+
# - The next version code corresponds to the `version`'s code incremented by one.
|
115
|
+
#
|
116
|
+
# @param [Hash] version The version hash for the current beta or release, containing values for keys "name" and "code"
|
117
|
+
# @param [Hash] alpha_version The version hash for the current alpha (defaultConfig), containing values for keys "name" and "code"
|
118
|
+
#
|
119
|
+
# @return [Hash] A version hash with keys "name" and "code", containing the version name and code to use for final release.
|
120
|
+
#
|
121
|
+
def self.calc_next_alpha_version(version, alpha_version)
|
122
|
+
# Bump alpha name
|
123
|
+
alpha_number = alpha_version[VERSION_NAME].sub(ALPHA_PREFIX, '')
|
124
|
+
alpha_name = "#{ALPHA_PREFIX}#{alpha_number.to_i() + 1}"
|
125
|
+
|
126
|
+
# Bump alpha code
|
127
|
+
alpha_code = version[VERSION_CODE] + 1
|
128
|
+
|
129
|
+
{ VERSION_NAME => alpha_name, VERSION_CODE => alpha_code }
|
130
|
+
end
|
131
|
+
|
132
|
+
# Compute the version name and code to use for the next beta (`X.Y.Z-rc-N`).
|
133
|
+
#
|
134
|
+
# - The next version name corresponds to the `version`'s name with the value after the `-rc-` suffix incremented by one,
|
135
|
+
# or with `-rc-1` added if there was no previous rc suffix (if `version` was not a beta but a release)
|
136
|
+
# - The next version code corresponds to the `alpha_version`'s (or `version`'s if `alpha_version` is nil) code, incremented by one.
|
137
|
+
#
|
138
|
+
# @example
|
139
|
+
# calc_next_beta_version({"name": "1.2.3", "code": 456}) #=> {"name": "1.2.3-rc-1", "code": 457}
|
140
|
+
# calc_next_beta_version({"name": "1.2.3-rc-2", "code": 456}) #=> {"name": "1.2.3-rc-3", "code": 457}
|
141
|
+
# calc_next_beta_version({"name": "1.2.3", "code": 456}, {"name": "alpha-1.2.3", "code": 457}) #=> {"name": "1.2.3-rc-1", "code": 458}
|
142
|
+
#
|
143
|
+
# @param [Hash] version The version hash for the current beta or release, containing values for keys "name" and "code"
|
144
|
+
# @param [Hash] alpha_version The version hash for the alpha, containing values for keys "name" and "code",
|
145
|
+
# or `nil` if no alpha version to consider.
|
146
|
+
#
|
147
|
+
# @return [Hash] A hash with keys `"name"` and `"code"` containing the next beta version name and code.
|
148
|
+
#
|
149
|
+
def self.calc_next_beta_version(version, alpha_version = nil)
|
150
|
+
# Bump version name
|
151
|
+
beta_number = is_beta_version?(version) ? version[VERSION_NAME].split('-')[2].to_i + 1 : 1
|
152
|
+
version_name = "#{version[VERSION_NAME].split('-')[0]}#{RC_SUFFIX}-#{beta_number}"
|
153
|
+
|
154
|
+
# Bump version code
|
155
|
+
version_code = alpha_version.nil? ? version[VERSION_CODE] + 1 : alpha_version[VERSION_CODE] + 1
|
156
|
+
{ VERSION_NAME => version_name, VERSION_CODE => version_code }
|
157
|
+
end
|
158
|
+
|
159
|
+
# Compute the version name to use for the next release (`"X.Y"`).
|
160
|
+
#
|
161
|
+
# @param [String] version The version name (string) to increment
|
162
|
+
#
|
163
|
+
# @return [String] The version name for the next release
|
164
|
+
#
|
165
|
+
def self.calc_next_release_short_version(version)
|
166
|
+
v = self.calc_next_release_base_version(VERSION_NAME => version, VERSION_CODE => nil)
|
167
|
+
return v[VERSION_NAME]
|
168
|
+
end
|
169
|
+
|
170
|
+
# Compute the next release version name for the given version, without incrementing the version code
|
171
|
+
#
|
172
|
+
# - The version name sees its minor version part incremented by one (and carried to next major if it reaches 10)
|
173
|
+
# - The version code is unchanged. This method is intended to be called internally by other methods taking care of the version code bump.
|
174
|
+
#
|
175
|
+
# @param [Hash] version A version hash, with keys `"name"` and `"code"`, containing the version to increment
|
176
|
+
#
|
177
|
+
# @return [Hash] Hash containing the next release version name ("X.Y") and code.
|
178
|
+
#
|
179
|
+
def self.calc_next_release_base_version(version)
|
180
|
+
version_name = remove_beta_suffix(version[VERSION_NAME])
|
181
|
+
vp = get_version_parts(version_name)
|
182
|
+
vp[MINOR_NUMBER] += 1
|
183
|
+
if vp[MINOR_NUMBER] == 10
|
184
|
+
vp[MAJOR_NUMBER] += 1
|
185
|
+
vp[MINOR_NUMBER] = 0
|
186
|
+
end
|
187
|
+
|
188
|
+
{ VERSION_NAME => "#{vp[MAJOR_NUMBER]}.#{vp[MINOR_NUMBER]}", VERSION_CODE => version[VERSION_CODE] }
|
189
|
+
end
|
190
|
+
|
191
|
+
# Compute the name of the next version to use after code freeze, by incrementing the current version name and making it a `-rc-1`
|
192
|
+
#
|
193
|
+
# @example
|
194
|
+
# calc_next_release_version({"name": "1.2", "code": 456}) #=> {"name":"1.3-rc-1", "code": 457}
|
195
|
+
# calc_next_release_version({"name": "1.2.3", "code": 456}) #=> {"name":"1.3-rc-1", "code": 457}
|
196
|
+
# calc_next_release_version({"name": "1.2", "code": 456}, {"name":"alpha-1.2", "code": 457}) #=> {"name":"1.3-rc-1", "code": 458}
|
197
|
+
#
|
198
|
+
# @param [Hash] version The current version hash, with keys `"name"` and `"code"`
|
199
|
+
# @param [Hash] alpha_version The current alpha version hash, with keys `"name"` and `"code"`, or nil if no alpha version
|
200
|
+
#
|
201
|
+
# @return [Hash] The hash containing the version name and code to use after release cut
|
202
|
+
#
|
203
|
+
def self.calc_next_release_version(version, alpha_version = nil)
|
204
|
+
nv = calc_next_release_base_version(VERSION_NAME => version[VERSION_NAME], VERSION_CODE => alpha_version.nil? ? version[VERSION_CODE] : [version[VERSION_CODE], alpha_version[VERSION_CODE]].max)
|
205
|
+
calc_next_beta_version(nv)
|
206
|
+
end
|
207
|
+
|
208
|
+
# Compute the name and code of the next hotfix version.
|
209
|
+
#
|
210
|
+
# @param [String] hotfix_version_name The next version name we want for the hotfix
|
211
|
+
# @param [String] hotfix_version_code The next version code we want for the hotfix
|
212
|
+
#
|
213
|
+
# @return [Hash] The predicted next hotfix version, as a Hash containing the keys `"name"` and `"code"`
|
214
|
+
#
|
215
|
+
def self.calc_next_hotfix_version(hotfix_version_name, hotfix_version_code)
|
216
|
+
{ VERSION_NAME => hotfix_version_name, VERSION_CODE => hotfix_version_code }
|
217
|
+
end
|
218
|
+
|
219
|
+
# Compute the name of the previous release version, by decrementing the minor version number
|
220
|
+
#
|
221
|
+
# @example
|
222
|
+
# calc_prev_release_version("1.2") => "1.1"
|
223
|
+
# calc_prev_release_version("1.2.3") => "1.1"
|
224
|
+
# calc_prev_release_version("3.0") => "2.9"
|
225
|
+
#
|
226
|
+
# @param [String] version The version string to decrement
|
227
|
+
#
|
228
|
+
# @return [String] A 2-parts version string "X.Y" corresponding to the guessed previous release version.
|
229
|
+
#
|
230
|
+
def self.calc_prev_release_version(version)
|
231
|
+
vp = get_version_parts(version)
|
232
|
+
if vp[MINOR_NUMBER] == 0
|
233
|
+
vp[MAJOR_NUMBER] -= 1
|
234
|
+
vp[MINOR_NUMBER] = 9
|
235
|
+
else
|
236
|
+
vp[MINOR_NUMBER] -= 1
|
237
|
+
end
|
238
|
+
|
239
|
+
"#{vp[MAJOR_NUMBER]}.#{vp[MINOR_NUMBER]}"
|
240
|
+
end
|
241
|
+
|
242
|
+
# Determines if a version name corresponds to a hotfix
|
243
|
+
#
|
244
|
+
# @param [String] version The version number to test
|
245
|
+
#
|
246
|
+
# @return [Bool] True if the version number has a non-zero 3rd component, meaning that it is a hotfix version.
|
247
|
+
#
|
248
|
+
def self.is_hotfix?(version)
|
249
|
+
return false if is_alpha_version?(version)
|
250
|
+
|
251
|
+
vp = get_version_parts(version[VERSION_NAME])
|
252
|
+
return (vp.length > 2) && (vp[HOTFIX_NUMBER] != 0)
|
253
|
+
end
|
254
|
+
|
255
|
+
# Prints the current and next release version names to stdout, then returns the next release version
|
256
|
+
#
|
257
|
+
# @return [String] The next release version name to use after bumping the currently used release version.
|
258
|
+
#
|
259
|
+
def self.bump_version_release
|
260
|
+
# Bump release
|
261
|
+
current_version = get_release_version()
|
262
|
+
UI.message("Current version: #{current_version[VERSION_NAME]}")
|
263
|
+
new_version = calc_next_release_base_version(current_version)
|
264
|
+
UI.message("New version: #{new_version[VERSION_NAME]}")
|
265
|
+
verified_version = verify_version(new_version[VERSION_NAME])
|
266
|
+
|
267
|
+
return verified_version
|
268
|
+
end
|
269
|
+
|
270
|
+
# Update the `build.gradle` file with new `versionName` and `versionCode` values, both or the `defaultConfig` and `vanilla` flavors
|
271
|
+
#
|
272
|
+
# @param [Hash] new_version_beta The version hash for the beta (vanilla flavor), containing values for keys "name" and "code"
|
273
|
+
# @param [Hash] new_version_alpha The version hash for the alpha (defaultConfig), containing values for keys "name" and "code"
|
274
|
+
# @env HAS_ALPHA_VERSION If set (with any value), indicates that the project uses `vanilla` flavor.
|
275
|
+
#
|
276
|
+
def self.update_versions(new_version_beta, new_version_alpha)
|
277
|
+
self.update_version(new_version_beta, ENV['HAS_ALPHA_VERSION'].nil? ? 'defaultConfig' : 'vanilla {')
|
278
|
+
self.update_version(new_version_alpha, 'defaultConfig') unless new_version_alpha.nil?
|
279
|
+
end
|
280
|
+
|
281
|
+
# Compute the name of the previous hotfix version.
|
282
|
+
#
|
283
|
+
# @param [String] version_name The current version name we want to decrement
|
284
|
+
#
|
285
|
+
# @return [String] The predicted previous hotfix version, in the form of "X.Y.Z", or "X.Y" if Z is 0.
|
286
|
+
# Corresponds to decrementing the 3rd component Z of the version, stripping it if it ends up being zero.
|
287
|
+
#
|
288
|
+
def self.calc_prev_hotfix_version_name(version_name)
|
289
|
+
vp = get_version_parts(version_name)
|
290
|
+
vp[HOTFIX_NUMBER] -= 1 unless vp[HOTFIX_NUMBER] == 0
|
291
|
+
return "#{vp[MAJOR_NUMBER]}.#{vp[MINOR_NUMBER]}.#{vp[HOTFIX_NUMBER]}" unless vp[HOTFIX_NUMBER] == 0
|
292
|
+
|
293
|
+
"#{vp[MAJOR_NUMBER]}.#{vp[MINOR_NUMBER]}"
|
294
|
+
end
|
295
|
+
|
296
|
+
# Extract the value of a import key from build.gradle
|
297
|
+
#
|
298
|
+
# @param [String] import_key The key to look for
|
299
|
+
# @return [String] The value of the key, or nil if not found
|
300
|
+
#
|
301
|
+
def self.get_library_version_from_gradle_config(import_key:)
|
302
|
+
gradle_file_path = File.join(ENV['PROJECT_ROOT_FOLDER'] || '.', 'build.gradle')
|
303
|
+
|
304
|
+
return nil unless File.exist?(gradle_file_path)
|
305
|
+
|
306
|
+
File.open(gradle_file_path, 'r') do |f|
|
307
|
+
text = f.read
|
308
|
+
text.match(/^\s*(?:\w*\.)?#{Regexp.escape(import_key)}\s*=\s*['"](.*?)["']/m)&.captures&.first
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
#----------------------------------------
|
313
|
+
private
|
314
|
+
|
315
|
+
# Remove the beta suffix (part after the `-`) from a version string
|
316
|
+
#
|
317
|
+
# @param [String] version The version string to remove the suffix from
|
318
|
+
#
|
319
|
+
# @return [String] The part of the version string without the beta suffix, i.e. the part before the first dash.
|
320
|
+
#
|
321
|
+
# @example remove_beta_suffix("1.2.3-rc.4") => "1.2.3"
|
322
|
+
#
|
323
|
+
def self.remove_beta_suffix(version)
|
324
|
+
version.split('-')[0]
|
325
|
+
end
|
326
|
+
|
327
|
+
# Split a version string into its individual integer parts
|
328
|
+
#
|
329
|
+
# @param [String] version The version string to split, e.g. "1.2.3.4"
|
330
|
+
#
|
331
|
+
# @return [Array<Int>] An array of integers containing the individual integer parts of the version.
|
332
|
+
# Always contains 3 items at minimum (0 are added to the end if the original string contains less than 3 parts)
|
333
|
+
#
|
334
|
+
def self.get_version_parts(version)
|
335
|
+
parts = version.split('.').map(&:to_i)
|
336
|
+
parts.fill(0, parts.length...3) # add 0 if needed to ensure array has at least 3 components
|
337
|
+
return parts
|
338
|
+
end
|
339
|
+
|
340
|
+
# Extract the versionName from a build.gradle file
|
341
|
+
#
|
342
|
+
# @param [String] file_path The path to the `.gradle` file
|
343
|
+
# @param [String] section The name of the section we expect the keyword to be in, e.g. "defaultConfig" or "vanilla"
|
344
|
+
#
|
345
|
+
# @return [String] The value of the versionName attribute as found in the build.gradle file and for this section.
|
346
|
+
#
|
347
|
+
def self.get_version_name_from_gradle_file(file_path, section)
|
348
|
+
res = get_keyword_from_gradle_file(file_path, section, 'versionName')
|
349
|
+
res = res.tr('\"', '') unless res.nil?
|
350
|
+
return res
|
351
|
+
end
|
352
|
+
|
353
|
+
# Extract the versionCode rom a build.gradle file
|
354
|
+
#
|
355
|
+
# @param [String] file_path The path to the `.gradle` file
|
356
|
+
# @param [String] section The name of the section we expect the keyword to be in, e.g. "defaultConfig" or "vanilla"
|
357
|
+
#
|
358
|
+
# @return [String] The value of the versionCode attribute as found in the build.gradle file and for this section.
|
359
|
+
#
|
360
|
+
def self.get_version_build_from_gradle_file(file_path, section)
|
361
|
+
res = get_keyword_from_gradle_file(file_path, section, 'versionCode')
|
362
|
+
return res.to_i
|
363
|
+
end
|
364
|
+
|
365
|
+
# Extract the value for a specific keyword in a specific section of a `.gradle` file
|
366
|
+
#
|
367
|
+
# @todo: This implementation is very fragile. This should be done parsing the file in a proper way.
|
368
|
+
# Leveraging gradle itself is probably the easiest way.
|
369
|
+
#
|
370
|
+
# @param [String] file_path The path of the `.gradle` file to extract the value from
|
371
|
+
# @param [String] section The name of the section from which we want to extract this keyword from. For example `defaultConfig` or `myFlavor`
|
372
|
+
# @param [String] keyword The keyword (key name) we want the value for
|
373
|
+
#
|
374
|
+
# @return [String] Returns the value for that keyword in the section of the `.gradle` file, or nil if not found.
|
375
|
+
#
|
376
|
+
def self.get_keyword_from_gradle_file(file_path, section, keyword)
|
377
|
+
found_section = false
|
378
|
+
File.open(file_path, 'r') do |file|
|
379
|
+
file.each_line do |line|
|
380
|
+
if !found_section
|
381
|
+
found_section = true if line.include?(section)
|
382
|
+
else
|
383
|
+
return line.split(' ')[1] if line.include?(keyword) && !line.include?("\"#{keyword}\"") && !line.include?("P#{keyword}")
|
384
|
+
end
|
385
|
+
end
|
386
|
+
end
|
387
|
+
return nil
|
388
|
+
end
|
389
|
+
|
390
|
+
# Ensure that a version string is correctly formatted (that is, each of its parts is a number) and returns the 2-parts version number
|
391
|
+
#
|
392
|
+
# @param [String] version The version string to verify
|
393
|
+
#
|
394
|
+
# @return [String] The "major.minor" version string, only with the first 2 components
|
395
|
+
# @raise [UserError] If any of the parts of the version string is not a number
|
396
|
+
#
|
397
|
+
def self.verify_version(version)
|
398
|
+
v_parts = get_version_parts(version)
|
399
|
+
|
400
|
+
v_parts.each do |part|
|
401
|
+
UI.user_error!('Version value can only contains numbers.') unless is_int?(part)
|
402
|
+
end
|
403
|
+
|
404
|
+
"#{v_parts[MAJOR_NUMBER]}.#{v_parts[MINOR_NUMBER]}"
|
405
|
+
end
|
406
|
+
|
407
|
+
# Check if a string is an integer.
|
408
|
+
#
|
409
|
+
# @param [String] string The string to test
|
410
|
+
#
|
411
|
+
# @return [Bool] true if the string is representing an integer value, false if not
|
412
|
+
#
|
413
|
+
def self.is_int? string
|
414
|
+
true if Integer(string) rescue false
|
415
|
+
end
|
416
|
+
|
417
|
+
# The path to the build.gradle file for the project.
|
418
|
+
#
|
419
|
+
# @env PROJECT_ROOT_FOLDER The path to the root of the project (the folder containing the `.git` directory).
|
420
|
+
# @env PROJECT_NAME The name of the project, i.e. the name of the subdirectory containing the project's `build.gradle` file.
|
421
|
+
#
|
422
|
+
# @return [String] The path of the `build.gradle` file inside the project subfolder in the project's repo
|
423
|
+
#
|
424
|
+
def self.gradle_path
|
425
|
+
UI.user_error!("You need to set the \`PROJECT_ROOT_FOLDER\` environment variable to the path to the project's root") if ENV['PROJECT_ROOT_FOLDER'].nil?
|
426
|
+
UI.user_error!("You need to set the \`PROJECT_NAME\` environment variable to the relative path to the project subfolder name") if ENV['PROJECT_NAME'].nil?
|
427
|
+
File.join(ENV['PROJECT_ROOT_FOLDER'], ENV['PROJECT_NAME'], 'build.gradle')
|
428
|
+
end
|
429
|
+
|
430
|
+
# Update both the versionName and versionCode of the build.gradle file to the specified version.
|
431
|
+
#
|
432
|
+
# @param [Hash] version The version hash, containing values for keys "name" and "code"
|
433
|
+
# @param [String] section The name of the section to update in the build.gradle file, e.g. "defaultConfig" or "vanilla"
|
434
|
+
#
|
435
|
+
# @todo This implementation is very fragile. This should be done parsing the file in a proper way.
|
436
|
+
# Leveraging gradle itself is probably the easiest way.
|
437
|
+
#
|
438
|
+
def self.update_version(version, section)
|
439
|
+
gradle_path = self.gradle_path
|
440
|
+
temp_file = Tempfile.new('fastlaneIncrementVersion')
|
441
|
+
found_section = false
|
442
|
+
version_updated = 0
|
443
|
+
File.open(gradle_path, 'r') do |file|
|
444
|
+
file.each_line do |line|
|
445
|
+
if !found_section
|
446
|
+
temp_file.puts line
|
447
|
+
found_section = true if line.include? section
|
448
|
+
else
|
449
|
+
if version_updated < 2
|
450
|
+
if line.include?('versionName') && !line.include?('"versionName"') && !line.include?('PversionName')
|
451
|
+
version_name = line.split(' ')[1].tr('\"', '')
|
452
|
+
line.sub!(version_name, version[VERSION_NAME].to_s)
|
453
|
+
version_updated = version_updated + 1
|
454
|
+
end
|
455
|
+
|
456
|
+
if line.include? 'versionCode'
|
457
|
+
version_code = line.split(' ')[1]
|
458
|
+
line.sub!(version_code, version[VERSION_CODE].to_s)
|
459
|
+
version_updated = version_updated + 1
|
460
|
+
end
|
461
|
+
end
|
462
|
+
temp_file.puts line
|
463
|
+
end
|
464
|
+
end
|
465
|
+
file.close
|
466
|
+
end
|
467
|
+
temp_file.rewind
|
468
|
+
temp_file.close
|
469
|
+
FileUtils.mv(temp_file.path, gradle_path)
|
470
|
+
temp_file.unlink
|
471
|
+
end
|
472
|
+
end
|
473
|
+
end
|
474
|
+
end
|
475
|
+
end
|