Skip to content

Commit 2803cc6

Browse files
committed
Add dual macOS release flavors
1 parent 1b3d9db commit 2803cc6

5 files changed

Lines changed: 164 additions & 37 deletions

File tree

fastlane/Matchfile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,20 @@ def env_or_default(key, default_value = nil)
44
value
55
end
66

7+
def env_csv_or_default(key, default_value)
8+
value = ENV[key].to_s.strip
9+
return default_value if value.empty?
10+
value.split(",").map(&:strip).reject(&:empty?)
11+
end
12+
713
git_url(ENV.fetch("MATCH_GIT_URL"))
814
storage_mode("git")
915
git_branch(env_or_default("MATCH_GIT_BRANCH", "main"))
1016
shallow_clone(true)
1117

1218
type("developer_id")
1319
platform("macos")
14-
app_identifier(["com.keplr.vizor"])
20+
app_identifier(env_csv_or_default("MATCH_APP_IDENTIFIER", ["com.keplr.vizor", "com.keplr.vizor.testnet"]))
1521
team_id("SZTB68DXM4")
1622
readonly(ENV["MATCH_READONLY"].to_s.downcase == "true")
1723

fastlane/macos/Fastfile

Lines changed: 135 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require "tempfile"
99
require "tmpdir"
1010
require "yaml"
1111
require "open3"
12+
require "spaceship"
1213
require_relative "../patches/developer_id_application_certificate_type_patch"
1314

1415
def shell_escape(value)
@@ -19,13 +20,63 @@ def workspace_root
1920
File.expand_path("../..", __dir__)
2021
end
2122

23+
MACOS_RELEASE_FLAVOR_CONFIGS = {
24+
"mainnet" => {
25+
app_name: "Vizor",
26+
artifact_name: "Vizor",
27+
bundle_id: "com.keplr.vizor",
28+
bundle_id_name: "Vizor",
29+
zcash_default_network: "main",
30+
volume_name: "Install Vizor Wallet"
31+
},
32+
"testnet" => {
33+
app_name: "Vizor Testnet",
34+
artifact_name: "Vizor-Testnet",
35+
bundle_id: "com.keplr.vizor.testnet",
36+
bundle_id_name: "Vizor Testnet",
37+
zcash_default_network: "test",
38+
volume_name: "Install Vizor Testnet Wallet"
39+
}
40+
}.freeze
41+
42+
DEFAULT_MACOS_RELEASE_FLAVORS = ["mainnet", "testnet"].freeze
43+
44+
def normalize_macos_release_flavor(flavor)
45+
normalized = flavor.to_s.strip.downcase
46+
return normalized if MACOS_RELEASE_FLAVOR_CONFIGS.key?(normalized)
47+
48+
UI.user_error!("Unsupported macOS release flavor: #{flavor}. Expected one of: #{MACOS_RELEASE_FLAVOR_CONFIGS.keys.join(", ")}")
49+
end
50+
51+
def macos_release_flavors
52+
raw = ENV["VIZOR_MACOS_FLAVORS"].to_s.strip
53+
if raw.empty?
54+
single_flavor = ENV["VIZOR_MACOS_FLAVOR"].to_s.strip
55+
return [normalize_macos_release_flavor(single_flavor)] unless single_flavor.empty?
56+
57+
return DEFAULT_MACOS_RELEASE_FLAVORS
58+
end
59+
60+
flavors = raw.split(",").map(&:strip).reject(&:empty?)
61+
UI.user_error!("VIZOR_MACOS_FLAVORS must include at least one flavor") if flavors.empty?
62+
flavors.map { |flavor| normalize_macos_release_flavor(flavor) }.uniq
63+
end
64+
65+
def single_macos_release_flavor(options = {})
66+
raw = options[:flavor] || ENV["VIZOR_MACOS_FLAVOR"]
67+
raw = raw.to_s.strip
68+
raw.empty? ? "mainnet" : normalize_macos_release_flavor(raw)
69+
end
70+
2271
def release_version_from_tag(tag)
2372
match = /\Arelease\/v(\d+\.\d+\.\d+)\z/.match(tag.to_s)
2473
UI.user_error!("Unsupported release tag format: #{tag}") unless match
2574
match[1]
2675
end
2776

28-
def release_metadata
77+
def release_metadata(flavor: "mainnet")
78+
flavor = normalize_macos_release_flavor(flavor)
79+
flavor_config = MACOS_RELEASE_FLAVOR_CONFIGS.fetch(flavor)
2980
pubspec = YAML.safe_load(File.read(File.join(workspace_root, "pubspec.yaml")))
3081
version_string = pubspec.fetch("version")
3182
version, build_number = version_string.split("+", 2)
@@ -38,21 +89,27 @@ def release_metadata
3889
end
3990

4091
asset_version = release_version_from_tag(tag)
41-
app_name = "Vizor"
92+
app_name = flavor_config.fetch(:app_name)
93+
artifact_name = flavor_config.fetch(:artifact_name)
4294
dist_dir = File.join(workspace_root, "dist", "macos")
4395
app_path = File.join(workspace_root, "build", "macos", "Build", "Products", "Release", "#{app_name}.app")
4496

4597
{
98+
flavor: flavor,
4699
app_name: app_name,
47-
bundle_id: "com.keplr.vizor",
100+
artifact_name: artifact_name,
101+
bundle_id: flavor_config.fetch(:bundle_id),
102+
bundle_id_name: flavor_config.fetch(:bundle_id_name),
103+
zcash_default_network: flavor_config.fetch(:zcash_default_network),
104+
volume_name: flavor_config.fetch(:volume_name),
48105
version: version,
49106
build_number: build_number,
50107
tag: tag,
51108
release_name: ENV["RELEASE_NAME"] || tag,
52109
dist_dir: dist_dir,
53110
app_path: app_path,
54-
zip_path: File.join(dist_dir, "#{app_name}-#{asset_version}-macos.zip"),
55-
dmg_path: File.join(dist_dir, "#{app_name}-#{asset_version}-macos.dmg")
111+
zip_path: File.join(dist_dir, "#{artifact_name}-#{asset_version}-macos.zip"),
112+
dmg_path: File.join(dist_dir, "#{artifact_name}-#{asset_version}-macos.dmg")
56113
}
57114
end
58115

@@ -164,6 +221,46 @@ def import_match_signing_assets!(output_dir, keychain)
164221
ensure_developer_id_identity!(keychain[:path])
165222
end
166223

224+
def ensure_bundle_id_exists!(metadata, api_key_path)
225+
Spaceship::ConnectAPI.token = Spaceship::ConnectAPI::Token.from(filepath: api_key_path)
226+
227+
if Spaceship::ConnectAPI::BundleId.find(metadata[:bundle_id])
228+
UI.message("Bundle ID #{metadata[:bundle_id]} already exists")
229+
return
230+
end
231+
232+
UI.important("Creating macOS Bundle ID #{metadata[:bundle_id]}")
233+
Spaceship::ConnectAPI::BundleId.create(
234+
name: metadata[:bundle_id_name],
235+
platform: Spaceship::ConnectAPI::BundleIdPlatform::MAC_OS,
236+
identifier: metadata[:bundle_id]
237+
)
238+
end
239+
240+
def macos_flutter_build_environment(metadata)
241+
{
242+
"FLUTTER_XCODE_PRODUCT_NAME" => metadata[:app_name],
243+
"FLUTTER_XCODE_PRODUCT_BUNDLE_IDENTIFIER" => metadata[:bundle_id]
244+
}.map { |key, value| "#{key}=#{shell_escape(value)}" }.join(" ")
245+
end
246+
247+
def plist_value(path, key)
248+
stdout, stderr, status = Open3.capture3("/usr/libexec/PlistBuddy", "-c", "Print :#{key}", path)
249+
UI.user_error!("Failed to read #{key} from #{path}: #{stderr}") unless status.success?
250+
stdout.strip
251+
end
252+
253+
def verify_built_app_metadata!(metadata)
254+
info_plist = File.join(metadata[:app_path], "Contents", "Info.plist")
255+
UI.user_error!("Missing Info.plist at #{info_plist}") unless File.exist?(info_plist)
256+
257+
actual_bundle_id = plist_value(info_plist, "CFBundleIdentifier")
258+
actual_bundle_name = plist_value(info_plist, "CFBundleName")
259+
260+
UI.user_error!("Expected bundle id #{metadata[:bundle_id]}, got #{actual_bundle_id}") unless actual_bundle_id == metadata[:bundle_id]
261+
UI.user_error!("Expected bundle name #{metadata[:app_name]}, got #{actual_bundle_name}") unless actual_bundle_name == metadata[:app_name]
262+
end
263+
167264
platform :mac do
168265
private_lane :validate_signing_environment do |options|
169266
readonly = options.fetch(:readonly, match_readonly?)
@@ -179,13 +276,15 @@ platform :mac do
179276
end
180277

181278
private_lane :prepare_signing do |options|
182-
metadata = release_metadata
279+
metadata = release_metadata(flavor: single_macos_release_flavor(options))
183280
keychain = setup_signing_keychain
184281
readonly = options.fetch(:readonly, match_readonly?)
185282
validate_signing_environment(readonly: readonly)
186283

187284
Dir.mktmpdir("match-signing-assets") do |output_dir|
188285
with_app_store_connect_api_key_path(required: !readonly) do |api_key_path|
286+
ensure_bundle_id_exists!(metadata, api_key_path) unless readonly
287+
189288
match_options = {
190289
type: "developer_id",
191290
app_identifier: metadata[:bundle_id],
@@ -219,8 +318,8 @@ platform :mac do
219318
)
220319
end
221320

222-
private_lane :notarize_app_bundle do
223-
metadata = release_metadata
321+
private_lane :notarize_app_bundle do |options|
322+
metadata = release_metadata(flavor: single_macos_release_flavor(options))
224323
UI.user_error!("Missing app bundle at #{metadata[:app_path]}") unless File.directory?(metadata[:app_path])
225324

226325
with_app_store_connect_api_key_path do |api_key_path|
@@ -236,8 +335,8 @@ platform :mac do
236335
sh("xcrun stapler staple #{shell_escape(metadata[:app_path])}")
237336
end
238337

239-
private_lane :notarize_dmg do
240-
metadata = release_metadata
338+
private_lane :notarize_dmg do |options|
339+
metadata = release_metadata(flavor: single_macos_release_flavor(options))
241340
UI.user_error!("Missing disk image at #{metadata[:dmg_path]}") unless File.exist?(metadata[:dmg_path])
242341

243342
with_app_store_connect_api_key_path do |api_key_path|
@@ -253,8 +352,11 @@ platform :mac do
253352
sh("xcrun stapler staple #{shell_escape(metadata[:dmg_path])}")
254353
end
255354

256-
private_lane :upload_github_release do
257-
metadata = release_metadata
355+
private_lane :upload_github_release do |options|
356+
flavors = options[:flavors] || macos_release_flavors
357+
metadata_items = flavors.map { |flavor| release_metadata(flavor: flavor) }
358+
metadata = metadata_items.first
359+
assets = metadata_items.flat_map { |item| [item[:zip_path], item[:dmg_path]] }
258360

259361
set_github_release(
260362
repository_name: ENV.fetch("RELEASE_REPOSITORY"),
@@ -265,29 +367,31 @@ platform :mac do
265367
is_draft: release_boolean_env("GITHUB_RELEASE_DRAFT"),
266368
is_prerelease: release_boolean_env("GITHUB_RELEASE_PRERELEASE"),
267369
is_generate_release_notes: true,
268-
upload_assets: [metadata[:zip_path], metadata[:dmg_path]]
370+
upload_assets: assets
269371
)
270372
end
271373

272374
desc "Build the macOS release app bundle"
273-
lane :build do
274-
metadata = release_metadata
375+
lane :build do |options|
376+
metadata = release_metadata(flavor: single_macos_release_flavor(options))
275377

276378
FileUtils.mkdir_p(metadata[:dist_dir])
277379

278380
sh("cd #{shell_escape(workspace_root)} && fvm flutter pub get")
279381
Bundler.with_unbundled_env do
280-
sh("cd #{shell_escape(workspace_root)} && fvm flutter build macos --release --no-pub")
382+
dart_define = shell_escape("ZCASH_DEFAULT_NETWORK=#{metadata[:zcash_default_network]}")
383+
sh("cd #{shell_escape(workspace_root)} && #{macos_flutter_build_environment(metadata)} fvm flutter build macos --release --no-pub --dart-define=#{dart_define}")
281384
end
282385

283386
UI.user_error!("Expected app bundle at #{metadata[:app_path]}") unless File.directory?(metadata[:app_path])
387+
verify_built_app_metadata!(metadata)
284388
end
285389

286390
desc "Package the macOS release build into ZIP and DMG"
287391
lane :package do |options|
288-
build unless options[:skip_build]
392+
build(flavor: single_macos_release_flavor(options)) unless options[:skip_build]
289393

290-
metadata = release_metadata
394+
metadata = release_metadata(flavor: single_macos_release_flavor(options))
291395
FileUtils.mkdir_p(metadata[:dist_dir])
292396
FileUtils.rm_f([metadata[:zip_path], metadata[:dmg_path]])
293397

@@ -303,22 +407,27 @@ platform :mac do
303407
"--app-path", shell_escape(metadata[:app_path]),
304408
"--output", shell_escape(metadata[:dmg_path]),
305409
"--app-name", shell_escape(metadata[:app_name]),
306-
"--volume-name", shell_escape("Install Vizor Wallet")
410+
"--volume-name", shell_escape(metadata[:volume_name])
307411
].join(" "))
308412
end
309413

310414
desc "Install macOS signing identities and provisioning profiles via match"
311415
lane :sync_signing do |options|
312-
prepare_signing(readonly: options.fetch(:readonly, match_readonly?))
416+
prepare_signing(readonly: options.fetch(:readonly, match_readonly?), flavor: single_macos_release_flavor(options))
313417
end
314418

315419
desc "Build, notarize, package, and upload the macOS release"
316420
lane :release do
317-
prepare_signing(readonly: match_readonly?)
318-
build
319-
notarize_app_bundle
320-
package(skip_build: true)
321-
notarize_dmg
322-
upload_github_release
421+
flavors = macos_release_flavors
422+
423+
flavors.each do |flavor|
424+
prepare_signing(readonly: match_readonly?, flavor: flavor)
425+
build(flavor: flavor)
426+
notarize_app_bundle(flavor: flavor)
427+
package(skip_build: true, flavor: flavor)
428+
notarize_dmg(flavor: flavor)
429+
end
430+
431+
upload_github_release(flavors: flavors)
323432
end
324433
end

fastlane/macos/README.md

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,20 @@ bundle exec fastlane mac release
1212

1313
`release` lane 순서:
1414

15-
1. `match`로 Developer ID 인증서 + provisioning profile 설치
16-
2. `fvm flutter build macos --release`
17-
3. `.app` notarize + staple
18-
4. `.zip` / `.dmg` 생성
19-
5. `.dmg` notarize + staple
20-
6. GitHub Release asset 업로드
15+
1. mainnet/testnet flavor별로 `match`로 Developer ID 인증서 + provisioning profile 설치
16+
2. flavor별 `fvm flutter build macos --release --dart-define=ZCASH_DEFAULT_NETWORK=<network>`
17+
3. flavor별 `.app` notarize + staple
18+
4. flavor별 `.zip` / `.dmg` 생성
19+
5. flavor별 `.dmg` notarize + staple
20+
6. GitHub Release에 모든 flavor asset 업로드
21+
22+
기본 release flavor는 `mainnet,testnet`입니다. 단일 flavor만 빌드하려면:
23+
24+
```bash
25+
VIZOR_MACOS_FLAVOR=mainnet bundle exec fastlane mac build
26+
VIZOR_MACOS_FLAVOR=testnet bundle exec fastlane mac build
27+
VIZOR_MACOS_FLAVORS=mainnet,testnet bundle exec fastlane mac release
28+
```
2129

2230
## Required environment variables
2331

@@ -41,7 +49,10 @@ bundle exec fastlane mac release
4149
- `MACOS_KEYCHAIN_NAME`
4250
- `MATCH_GIT_BRANCH`
4351
- `MATCH_READONLY`
52+
- `MATCH_APP_IDENTIFIER`
4453
- `RELEASE_NAME`
54+
- `VIZOR_MACOS_FLAVOR`
55+
- `VIZOR_MACOS_FLAVORS`
4556
- `GITHUB_RELEASE_DRAFT`
4657
- `GITHUB_RELEASE_PRERELEASE`
4758

@@ -51,4 +62,5 @@ bundle exec fastlane mac release
5162
- `MATCH_GIT_BASIC_AUTHORIZATION`은 GitHub private repo 접근용 Basic auth Base64 문자열입니다. 예: `echo -n "github_user:pat" | base64`
5263
- CI는 `MATCH_READONLY=true`로 두고, `match` 저장소는 로컬에서 한 번 시드해 둔 상태를 전제로 합니다.
5364
- `MATCH_READONLY=false`로 돌리면 fastlane은 `match` write 모드로 동작합니다. 이때는 `APP_STORE_CONNECT_API_KEY_JSON`이 준비돼 있어야 하며, git commit identity는 deployment workflow가 설정합니다.
65+
- `MATCH_READONLY=false`일 때 testnet Bundle ID(`com.keplr.vizor.testnet`)가 없으면 fastlane이 App Store Connect API로 먼저 생성한 뒤 Developer ID provisioning profile을 생성합니다.
5466
- 산출물은 `/Users/junghwanyun/zcash-wallet/dist/macos` 아래에 생성됩니다.

macos/Runner/DebugProfile.entitlements

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
<plist version="1.0">
44
<dict>
55
<key>com.apple.application-identifier</key>
6-
<string>SZTB68DXM4.com.keplr.vizor</string>
6+
<string>$(DEVELOPMENT_TEAM).$(PRODUCT_BUNDLE_IDENTIFIER)</string>
77
<key>com.apple.developer.team-identifier</key>
8-
<string>SZTB68DXM4</string>
8+
<string>$(DEVELOPMENT_TEAM)</string>
99
<key>com.apple.security.app-sandbox</key>
1010
<true/>
1111
<key>com.apple.security.cs.allow-jit</key>

macos/Runner/Release.entitlements

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
<plist version="1.0">
44
<dict>
55
<key>com.apple.application-identifier</key>
6-
<string>SZTB68DXM4.com.keplr.vizor</string>
6+
<string>$(DEVELOPMENT_TEAM).$(PRODUCT_BUNDLE_IDENTIFIER)</string>
77
<key>com.apple.developer.team-identifier</key>
8-
<string>SZTB68DXM4</string>
8+
<string>$(DEVELOPMENT_TEAM)</string>
99
<key>com.apple.security.app-sandbox</key>
1010
<true/>
1111
<key>com.apple.security.network.client</key>

0 commit comments

Comments
 (0)