-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapple.cppm
More file actions
1591 lines (1510 loc) · 85 KB
/
Copy pathapple.cppm
File metadata and controls
1591 lines (1510 loc) · 85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// mcpp.dist.apple -- a staged tree becomes a `.app` bundle, and is optionally
// signed.
//
// WHY THIS IS NEITHER A RULE NOR A TOOL. A rule states how a translation unit
// is compiled by a compiler mcpp does not drive. A tool states something the
// build program needs that no compiler performs, and does it while the
// program runs. This member does neither: it consumes a STAGED TREE -- itself
// built from link outputs -- and produces something a user installs. That is
// the third category `dist/appimage.cppm` establishes, and the prefix says
// which of the three questions a member answers.
//
// THE ENGINE HOLDS THE DISPATCH AND NOT THE FORMAT. `mcpp pack --format app`
// finds the package that declared the name; nothing about `Info.plist`'s
// keys, the bundle layout, or `codesign`'s flags is in mcpp. Apple's own
// notarisation service is a further release mcpp does not control even less
// than the bundle format is, which is one reason it is out of scope below.
//
// UNLIKE AN AppImage, THIS IS A RE-LAYOUT, AND THAT IS WHY IT COSTS MORE THAN
// THREE FILES. `mcpp pack --mode vendored` stages `bin/`, `lib/` and a
// top-level launcher -- a tree that already satisfies what an AppImage wants,
// because an AppImage has no opinion about its own internal layout beyond
// `AppRun`. A `.app` bundle does have an opinion: it wants the executable and
// its closure under `Contents/MacOS/`, configuration under `Contents/`, and
// resources under `Contents/Resources/`. Reaching that from the staged tree
// is therefore a MOVE, not an ADDITION, and a build program is a bad place to
// perform it: the staged tree can be hundreds of megabytes, and a build
// program is a poor substitute for a shell utility written and optimised for
// exactly this copy. So the assembly is done as the declared COMMAND of one
// or more actions, which is a graph edge ninja schedules and can skip on a
// cache hit, rather than as file I/O this program performs every time it
// runs.
//
// `ditto` IS THE TOOL FOR EVERY COPY BELOW, AND FOR ONE STATED REASON: `cp -R
// SRC DST`'s meaning depends on whether `DST` already exists -- copying
// SRC's CONTENTS into an existing `DST`, but creating a new `DST` as a peer
// of `SRC` when it does not -- which is exactly the kind of environment-
// dependent behaviour that turns "it worked when I tested it" into "it
// nested one directory deeper on a clean machine". `ditto SRC DST` does not
// have that branch: `DST`'s contents become a copy of `SRC`'s contents either
// way, it creates every intermediate directory `DST` needs, and it preserves
// resource forks and permissions, which a plain `cp -R` is not guaranteed to.
// It is part of the base macOS install, not Xcode, so it needs no discovery
// and no `options::tool` the way `wix` does in `dist/wix.cppm` -- there is
// exactly one `ditto`, at a fixed path, on every Mac this can run on.
//
// HOW MANY ACTIONS, AND WHY EACH IS SEPARATE:
//
// 1. install `Info.plist` (always)
// 2. lay out the program (always)
// 3. one per deployed resource (one per entry the staged tree carries)
// 4. one per closure dylib (copied to the framework directory, signed)
// 5. install the icon (only when `options::icon` is set)
// 6. codesign the bundle (always on macOS; on an iOS device row only
// when `options::identity` is set)
// 7. the bundle itself (always, last)
// 8. two for `--format dmg` (stage the bundle beside an `Applications`
// link, then `hdiutil create`)
//
// 1, 3 and 5 are separate from 2 because they have different INPUTS:
// `Info.plist` is regenerated whenever package metadata changes, the icon only
// when the project's icon file changes, and the program only when it is
// relinked. One action for all of them would make every one of those changes
// re-run every copy. 6 is last of the CONTENT steps and depends on the OUTPUTS
// of every step before it, because a code signature covers the bundle's content
// at signing time -- signing before the content is in place is either a failure
// (an incomplete bundle) or a signature that the next file added invalidates.
//
// 7 EXISTS BECAUSE THE CONTENT STEPS ARE PARALLEL, AND `mcpp run` NEEDS ONE
// OPERAND. Each of them writes a file inside the bundle and consumes none of
// the others' outputs, so a request that submits only those has as many
// terminal artifacts (outputs nothing else consumes) as steps actually ran,
// never one. `mcpp run --format app` resolves to THE terminal
// artifact, so a plan with more than one has none it can hand the runner.
// Step 7's output is the bundle DIRECTORY -- the actual distributable of this
// format -- and its inputs are every other step's output, so it is always
// the plan's sole terminal, in both the `Contents/`-shaped and flat-iOS
// layouts. Under `--format dmg` the bundle is an input of the image, and the
// `.dmg` is the terminal instead.
//
// `Info.plist` IS WRITTEN AT PLAN TIME, BUT NOT DIRECTLY TO ITS FINAL PATH,
// AND THE DIFFERENCE MATTERS. It is configuration, so `write_if_different`
// is the right way to produce its bytes -- the same reasoning
// `dist/appimage.cppm` gives for its own desktop entry. But this member
// writes it to a location of its own choosing and declares that file as the
// INPUT of the action that copies it into `Contents/Info.plist`, rather than
// writing directly to the bundle path. A stray write to a path no action
// declares is invisible to the graph: nothing would notice `Info.plist`
// changing, and `dist/appimage.cppm`'s own header comment already measured
// what that costs -- "a metadata-only change would leave the previous
// [artifact] in place, reported as up to date." Declaring the plan-time file
// as this action's input is what makes a version bump reach the bundle.
//
// ON macOS THE BUNDLE IS SIGNED AD HOC UNLESS AN IDENTITY IS GIVEN. A bundle
// that carries a framework and no signature fails `codesign --verify --deep
// --strict` ("code has no resources but signature indicates they must be
// present", measured on macos-15, mcpp#635 run 2): the program the linker
// signed is sealed, and the bundle around it is not. Signing ad hoc needs no
// identity and no keychain, so it is the default, dylibs first and the bundle
// second (the same run measured that order verifying). `options::identity`
// signs with that identity instead, with `--timestamp`. Notarisation is out of
// scope entirely, and not merely deferred: it requires uploading the bundle to
// Apple over the network and waiting on a ticket, and a build must not reach
// the network -- the same rule `dist/appimage.cppm` states for appimagetool's
// runtime-stub download, here applying to a step this member does not attempt.
//
// THE CLOSURE'S DYLIBS GO TO THE FRAMEWORK DIRECTORY (mcpp 2026.9.14.2+). The
// engine reads a Mach-O program's closure and stages the dylibs it resolves
// beside the program in `bin/`, naming each in the stage manifest's `needs`
// lines. This member copies those into `Contents/Frameworks/` (`Frameworks/` on
// iOS), keeps them out of the resource directory, and gives the program the
// rpath that finds them there -- `@executable_path/../Frameworks`, or
// `@executable_path/Frameworks` on iOS -- through `mcpp::link_flag` at link
// time, so no file is edited after it is linked and no load command is
// rewritten. An rpath edit after the link was measured and not taken: it fails
// on a program linked without header padding ("larger updated load commands do
// not fit") and invalidates the linker's signature (mcpp#635 run 4). The rpath
// is added to every link of a macOS or iOS program whose build program calls
// `generate()`, packed or not, because the link happens before the pass that
// learns `--format`.
//
// `mcpp run --format app` REACHES THE BUNDLE THROUGH `macapp-run` ON macOS.
// This member supplies the runner named `app` (`xim:macapp-run`, which executes
// the bundle's `CFBundleExecutable` in the foreground, so its output and exit
// status are the program's), and mcpp uses the runner named after a format for
// that format (mcpp 2026.9.14.2+). A project that declares its own
// `[target.<triple>.runners] app` keeps it. The iOS rows keep the runner their
// manifests name (`simctl-run`).
//
// `--format dmg` IS A DISK IMAGE OF THE BUNDLE. The bundle and a link to
// `/Applications` are staged in one directory and `hdiutil create -format UDZO`
// writes the image, the layout a user drags from. Measured on macos-15
// (mcpp#635 run 2): the image is created, `hdiutil verify` accepts it, and it
// attaches read-only with the bundle and the link. macOS only.
//
// iOS IS THE SAME SHAPE, A FLAT LAYOUT INSTEAD OF `Contents/`, AND THREE
// KEYS `-format app` NEVER WROTE (#622 B1). The target row and the SDK
// (`aarch64-ios-sim`, `aarch64-ios`) are the engine's; what was missing here
// was the branch, not a mechanism -- every action below is the same four
// steps the header above already lists, addressed at the bundle's own root
// rather than `Contents/`.
//
// FLAT, BECAUSE THAT IS WHAT AN iOS BUNDLE IS. There is no `Contents/`
// subdirectory on this platform: the executable, `Info.plist` and every
// resource sit directly under `<Name>.app/`. So `execDir`, `plistDst` and
// `resourceDir` below are the bundle root itself on this branch and
// `Contents/MacOS`, `Contents/Info.plist`, `Contents/Resources` on the
// macOS one -- one predicate, read once, rather than four `os == "macos"`
// checks scattered through the function.
//
// `CFBundleSupportedPlatforms` READS THE SIMULATOR FROM `target_env()`, NOT
// FROM A SEPARATE OPTION. `aarch64-ios-sim` and `aarch64-ios` are two
// triples for one OS (`triple.cppm`'s own comment: "the simulator is
// deliberately not a row [of its own identity]... it has its own SDK"), so
// the engine already carries the distinction this key needs; restating it
// as an `options` field would be a second copy of what `target_env()`
// answers.
//
// SIGNING SPLITS ON THE SAME PREDICATE. A simulator bundle installs
// unsigned -- `simctl install` does not check a signature -- so
// `options::identity` is ignored there rather than attempted and left to
// fail inside `codesign`, which cannot produce a device-shaped signature
// for a simulator binary in any case. The device row signs only with an
// identity, because a device refuses an ad-hoc signature; its `codesign`
// step is the macOS one with an identity: same argv shape, same
// hardened-runtime and entitlements flags.
//
// ICONS TAKE A DIRECTORY ON THIS ROW, A FILE ON THE OTHER. macOS names one
// `.icns`; iOS's convention is a set of flat PNGs at several pixel sizes,
// listed by stem under `CFBundleIcons` / `CFBundlePrimaryIcon` /
// `CFBundleIconFiles`. Generating the required sizes from a source image is
// Xcode's `actool`, which is not redistributable and not reimplemented
// here (the same boundary `codesign` and `wix.exe` already draw): this
// member copies whatever PNGs the project already has into the bundle root
// and lists their stems. A project supplying the wrong sizes gets a bundle
// that installs and a Home Screen icon Apple's UI does not like -- a
// cosmetic failure, not a build one.
//
// A PROJECT'S OWN Info.plist ENTRIES, AND A DEVICE BUNDLE THAT INSTALLS (0.11.0).
// `options::info_plist` names a plist whose top-level entries join the bundle's
// -- the usage descriptions, URL types and background modes an Xcode project's
// Info.plist carries -- while the keys this member derives stay its own and are
// refused by name. On the iOS device row, `options::provisioning_profile` is
// embedded as `embedded.mobileprovision` and, unless `options::entitlements`
// names a file, supplies the entitlements the bundle is signed with; the
// profile's plist is read from the file itself, so a plan made on any host
// checks the bundle identifier against it. `mcpp run --format app` on that row
// reaches the device through the runner named `app` this member supplies,
// `devicectl-run` (`xim:apple-device-tools`), as `macapp-run` serves macOS.
module;
#include <cstdio>
export module mcpp.dist.apple;
import std;
import mcpp;
import mcpp.plugins;
// Nothing here uses `std::println`, and that is not a style choice: both of
// its overloads reach into the libc++ dylib for symbols macOS 14 does not
// ship, so a member that printed with it compiled and then failed to link.
// `std::format` is header-only. The full measurement is in `rules/spirv.cppm`.
export namespace mcpp::dist::apple {
// ─── Options ───────────────────────────────────────────────────────────────
struct options {
// The program target this bundle wraps. Empty means the package name,
// which is the target `mcpp pack` itself selects by convention.
std::string target;
// The bundle's own name -- the `<Name>` in `<Name>.app`, and
// `CFBundleName`. Empty means the target name, then the package name,
// then "app".
std::string app_name;
// `CFBundleIdentifier`. Empty derives a reversed-DNS-shaped identifier
// from `package_namespace()` and `package_name()`; see `bundle_id_for`.
std::string bundle_id;
// `CFBundleShortVersionString` and `CFBundleVersion`, used as given, with
// no conversion: unlike `dist/wix.cppm`'s MSI version, Apple's bundle
// version keys have no field-width ceiling this member has measured, so
// mcpp's own four-segment date version needs nothing done to it here.
// Empty means `package_version()`.
std::string version;
// On macOS: a project-supplied icon FILE, copied into
// `Contents/Resources/` and named by `CFBundleIconFile`. Finder
// specifically expects `.icns` (or the newer `.icon` bundle) to render
// an application icon; this member does not validate or convert the
// format, only wires up whatever file is named. Empty omits
// `Contents/Resources/` and `CFBundleIconFile` entirely -- macOS runs a
// bundle with no custom icon without complaint.
//
// On iOS: a project-supplied DIRECTORY of flat PNGs -- a file here is
// refused, naming the directory shape iOS expects. Every `*.png` in it
// is copied to the bundle's root and its stem (the filename without
// `.png`) is listed under `CFBundleIcons` / `CFBundlePrimaryIcon` /
// `CFBundleIconFiles`, so a project names its icon set once, at
// whatever sizes it has generated, rather than once per size in this
// member's own vocabulary. Empty omits the bundle icon entirely, as on
// macOS.
std::string icon;
// `LSMinimumSystemVersion` on macOS, `MinimumOSVersion` on iOS.
//
// ON iOS THIS IS AN OVERRIDE, NOT THE ONLY SOURCE. mcpp 2026.9.12.2
// exposes the compiled deployment target as `mcpp::min_platform_version()`
// (#622 A11) -- the same value the linked Mach-O's `LC_BUILD_VERSION`
// carries -- and this member reads it first, falling back to this field
// only when the engine reports nothing (a target row the engine does not
// yet compute a floor for). A project therefore states
// `[build] ios_deployment_target` once and this key is free.
//
// ON macOS THIS STAYS THE ONLY SOURCE, deliberately: `min_platform_version`
// answers non-empty on macOS only when mcpp itself runs on a Mac
// (`mcpp::platform::macos::deployment_target` is guarded `#if defined
// (__APPLE__)`, because `MACOSX_DEPLOYMENT_TARGET` and the SDK default
// it falls back to are both properties of the machine RUNNING mcpp, not
// of the target triple), so reading it here would make a `.app` built
// by a Linux packaging host silently lose the key a macOS host would
// have set. Extending the engine reading to macOS is future work the
// design record recommends and this member does not take, so that the
// existing macOS fixture's bundle is unchanged by this row's addition.
std::string minimum_system_version;
// A `codesign` identity -- a name or hash `security find-identity` would
// list. Empty means an ad-hoc signature on macOS and no signature on the
// iOS rows; see the header comment's signing paragraphs. Ignored on the
// iOS Simulator row regardless of this value, and a non-empty value there
// produces a `mcpp::warning` naming why rather than a signature.
std::string identity;
// `--options runtime`, the hardened runtime, only meaningful together
// with `identity`: without signing there is no runtime flag to attach it
// to.
bool hardened_runtime = false;
// `--entitlements <path>`, likewise only applied when `identity` is also
// set. Validated to exist when named, the same as `icon`.
std::string entitlements;
// A PLIST OF THE PROJECT'S OWN Info.plist ENTRIES (0.11.0), manifest-relative
// or absolute: every entry of its top-level `<dict>` joins the bundle's
// Info.plist. The keys this member derives from the options and the engine
// -- `CFBundleExecutable`, `CFBundleIdentifier`, `CFBundleName`, the two
// version keys, `CFBundlePackageType`, the minimum OS keys,
// `CFBundleSupportedPlatforms`, `CFBundleIconFile` and `CFBundleIcons` -- are
// refused by name, because an option or the engine states each; the three
// it only defaults (`UIDeviceFamily`, `LSRequiresIPhoneOS`,
// `NSHighResolutionCapable`) are replaced by the project's value.
std::string info_plist;
// DEFAULTED Info.plist KEYS TO LEAVE OUT (0.12.0). A property list has no
// null, so `info_plist` can replace a default's value but cannot remove the
// key; a project whose other build states neither `NSHighResolutionCapable`
// nor `LSRequiresIPhoneOS` names them here (#649 P1). Only the three keys
// this member defaults are accepted: a key it derives is refused by name, as
// `info_plist` refuses it, and so is any other key, which this member never
// writes. A key named here and also set by `info_plist` is refused, because
// the two statements contradict each other. A key that does not apply to the
// row (`UIDeviceFamily` on macOS) is accepted and changes nothing.
std::vector<std::string> omit_keys;
// Info.plist ENTRIES THE GRAPH CONTRIBUTES (0.12.0). With mcpp 2026.9.16.1
// the root project's build program receives the resolved graph, and every
// package in it other than the application that states
// `[package.metadata.dist-apple] info_plist = "<file>"` contributes that
// plist's entries, the path relative to the package's directory: a library
// that needs a usage description or a background mode states it once, and
// every application that depends on it carries it. The entries are applied
// in the graph's order, dependencies first, so a package overrides the
// packages it depends on, and the application's own `info_plist` is applied
// last and wins every key. The keys this member derives are refused in a
// contribution as in `info_plist`, naming the package; a key the application
// names in `omit_keys` is left out whoever contributes it.
//
// `false` reads no contribution. Under an older engine, or in a dependency's
// own build program, there is no graph, and nothing is contributed either
// way.
bool graph_info_plist = true;
// AN iOS DEVICE BUNDLE'S PROVISIONING PROFILE (0.11.0), manifest-relative or
// absolute: embedded as `embedded.mobileprovision`, and, when `entitlements`
// is empty, the source of the entitlements the bundle is signed with (the
// profile's own `Entitlements` dictionary). Its `application-identifier`
// must cover `bundle_id`. Only the device row (`aarch64-ios`) takes one, and
// it needs `identity`: a device refuses an ad-hoc signature.
std::string provisioning_profile;
// Where the produced bundle lands. Empty means `<out_dir>/<app_name>.app`.
std::string output;
// `--format dmg`: the volume's name, and where the image lands. Empty
// means `app_name` and `<out_dir>/<app_name>.dmg`.
std::string volume_name;
std::string dmg;
std::string out_dir = std::string(mcpp::out_dir());
};
// ─── The plan ──────────────────────────────────────────────────────────────
// One chained action per row of the header comment's table, and a submit
// that walks them in dependency order. `generate()` being exactly
// `submit(plan_for())` is what keeps a project's edit -- an extra resource,
// a different signing flag -- from becoming a reimplementation of this
// member, the same trade `dist/appimage.cppm` makes.
struct step {
// Owned strings rather than literals: the resource steps below name one
// action per deployed entry, and an id derived from a file name has to
// outlive the function that formed it.
std::string id;
const char* role;
std::string description;
std::vector<std::string> argv;
std::vector<std::string> inputs;
// MORE THAN ONE OUTPUT ON THE iOS ICON STEP: a directory of PNGs copies
// to a directory of PNGs, and the graph rule is "name the output
// files" -- plural, when a single command produces several. Every other
// step still declares exactly one; a vector costs those nothing.
std::vector<std::string> outputs;
};
struct plan {
// False when this build is not `mcpp pack --format app`, which is every
// ordinary build. `reason` then says which of the several ways.
bool applies = false;
std::string reason;
std::string bundle_path; // the <Name>.app directory
std::string dmg_path; // the .dmg, under --format dmg
std::string appdir; // pack_stage_dir(), kept for the floor check
std::vector<step> steps;
explicit operator bool() const { return applies; }
};
// ─── Internals ─────────────────────────────────────────────────────────────
inline bool is_file(const std::string& p) {
std::error_code ec;
return !p.empty() && std::filesystem::is_regular_file(p, ec);
}
// Written only when the bytes differ, for the reason `dist/appimage.cppm`
// gives: rewriting identical bytes moves the mtime, and a moved mtime on a
// declared input is indistinguishable from a changed one -- so a second pack
// of an unchanged project would re-copy the bundle and re-sign it.
inline bool write_if_different(const std::filesystem::path& path,
std::string_view bytes) {
std::error_code ec;
std::filesystem::create_directories(path.parent_path(), ec);
if (std::ifstream in(path, std::ios::binary); in) {
std::string old((std::istreambuf_iterator<char>(in)),
std::istreambuf_iterator<char>());
if (old == bytes) return true;
}
std::ofstream out(path, std::ios::binary | std::ios::trunc);
if (!out) return false;
out.write(bytes.data(), static_cast<std::streamsize>(bytes.size()));
return static_cast<bool>(out);
}
// Records a refusal on stderr and as a `mcpp::warning`. A member that refuses
// submits no action and its build program exits 0, and the engine discards the
// output of a build program that succeeded; its own error, "no action claimed
// --format 'app'", names no reason. The warning channel is one line per
// directive, so line breaks are folded into spaces.
inline plan& refuse(plan& p, std::string reason, const std::string& message) {
std::cerr << message << '\n';
std::string folded;
folded.reserve(message.size());
bool space = false;
for (std::size_t i = 0; i < message.size(); ++i) {
const char c = message[i];
if (c == '\n' || c == '\r') { space = true; continue; }
if (space) {
if (c == ' ') continue;
folded += ' ';
space = false;
}
folded += c;
}
mcpp::warning(folded.c_str());
p.reason = std::move(reason);
return p;
}
// A helper script, written into `<out_dir>/dist-apple/` at plan time when its
// bytes differ, and run by `/bin/sh` so that no permission bit is needed.
inline std::string helper_script(const std::string& out_dir, const char* name,
std::string_view body) {
const auto path = std::filesystem::path(out_dir) / "dist-apple" / name;
write_if_different(path, body);
return path.string();
}
inline std::string target_for(const options& opt) {
if (!opt.target.empty()) return opt.target;
const char* n = mcpp::package_name();
return (n && *n) ? std::string(n) : std::string();
}
inline std::string app_name_for(const options& opt) {
if (!opt.app_name.empty()) return opt.app_name;
if (!opt.target.empty()) return opt.target;
const char* n = mcpp::package_name();
return (n && *n) ? std::string(n) : std::string("app");
}
// The program a bundle executes, found in the staged tree: `bin/<target>`
// first, then the tree's top-level entry, then `run.sh`.
//
// THE PROGRAM, NOT THE TREE'S ENTRY SCRIPT. From the release that reads a
// Mach-O program's closure (mcpp 2026.9.14.2), the engine also writes
// `<tree>/<target>`, a shell script that executes `bin/<target>` from the
// tree's root. The search used to take that root entry first, which is the
// order `dist/appimage.cppm` needs, and a bundle then executed the script,
// which executed `Contents/MacOS/bin/<target>` -- a file no bundle carries:
// "cannot execute: No such file or directory", exit 126 (macos-15, run
// 34821164486). The script has no work to do inside a bundle, where
// `CFBundleExecutable` names the program directly, and the framework rpath
// `@executable_path/../Frameworks` resolves against the program's own
// directory, which has to be `Contents/MacOS/`. The root entry and `run.sh`
// remain for a tree that carries no `bin/<target>`.
inline std::string launcher_in(const std::string& stage, const std::string& target) {
for (auto candidate : {stage + "/bin/" + target,
stage + "/" + target,
stage + "/run.sh"})
if (is_file(candidate)) return candidate;
return {};
}
// CFBundleExecutable MUST BE A BARE FILENAME, NOT A PATH.
//
// Apple's own bundle documentation describes it as the executable's name
// within `Contents/MacOS/`, and real-world bundle tooling has hit this
// directly enough to be a filed CMake defect: "CFBundleExecutable path in a
// bundle should not be a relative path into bundle." So this member takes
// only the basename of whatever `launcher_in` finds, and the layout step
// copies that file to the top of the executable directory under that name, so
// the name and the file agree wherever in the staged tree it was found.
inline std::string bundle_executable_name(const std::string& launcher_path) {
return std::filesystem::path(launcher_path).filename().string();
}
inline std::string sanitize_bundle_id_component(std::string_view s) {
std::string out;
out.reserve(s.size());
for (char c : s) {
bool ok = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
|| (c >= '0' && c <= '9') || c == '-' || c == '.';
out += ok ? c : '-';
}
return out;
}
// A reversed-DNS-SHAPED identifier, not a literal reversal of a domain --
// mcpp packages carry `(namespace, name)`, not a domain to reverse. Sanitised
// to the character set Apple documents for `CFBundleIdentifier`
// (alphanumeric, `-`, `.`), because a package name or namespace is free text
// as far as this member is concerned.
inline std::string bundle_id_for(const options& opt) {
if (!opt.bundle_id.empty()) return opt.bundle_id;
const char* nsC = mcpp::package_namespace();
const char* nmC = mcpp::package_name();
const std::string ns = (nsC && *nsC) ? sanitize_bundle_id_component(nsC) : std::string();
const std::string nm = (nmC && *nmC) ? sanitize_bundle_id_component(nmC) : std::string();
if (!ns.empty() && !nm.empty()) return ns + "." + nm;
if (!nm.empty()) return nm;
if (!ns.empty()) return ns;
return "app";
}
inline std::string plist_escape(std::string_view s) {
std::string out;
out.reserve(s.size());
for (char c : s) {
switch (c) {
case '&': out += "&"; break;
case '<': out += "<"; break;
case '>': out += ">"; break;
default: out += c;
}
}
return out;
}
// Written as XML text directly rather than by shelling out to `plutil`: the
// content is a handful of string keys this member already holds, and adding
// a second host tool to discover and invoke would buy nothing over
// formatting the lines by hand. `LSMinimumSystemVersion` / `MinimumOSVersion`
// and `CFBundleIconFile` are omitted rather than emitted empty when their
// inputs are empty -- an empty string in either key is a claim as
// unsupported as omitting the key, so omission is the honest one.
//
// `is_ios` AND `is_sim` DECIDE FOUR KEYS, NOT ONE BRANCH. `MinimumOSVersion`
// replaces `LSMinimumSystemVersion`; `CFBundleSupportedPlatforms`,
// `UIDeviceFamily` and `LSRequiresIPhoneOS` are iOS-only and absent from
// every macOS bundle this member has ever written, which is the
// byte-identical property the macOS fixture depends on; `NSHighResolutionCapable`
// is a macOS concept (Retina-aware drawing on a platform that also has
// non-Retina displays) with nothing to opt into on iOS, so it is macOS-only
// in the other direction.
inline std::string plist_document(const std::string& executable, const std::string& bundle_id,
const std::string& name, const std::string& version,
bool is_ios, bool is_sim,
const std::string& min_os_version,
// The project's own entries (0.11.0), already
// formatted, inserted before `</dict>`; and the
// defaulted keys they replace.
const std::string& extra_entries,
const std::vector<std::string>& replaced_keys,
// macOS: the icon FILE's basename, extension
// included, exactly as `CFBundleIconFile` has
// always taken it. iOS: unused (see
// `ios_icon_stems`) and always empty.
const std::string& mac_icon_name,
// iOS: every PNG stem found in
// `options::icon`'s directory. macOS: unused
// and always empty.
const std::vector<std::string>& ios_icon_stems) {
std::string doc;
doc += "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
doc += "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" "
"\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n";
doc += "<plist version=\"1.0\">\n<dict>\n";
doc += std::format(" <key>CFBundleExecutable</key>\n <string>{}</string>\n",
plist_escape(executable));
doc += std::format(" <key>CFBundleIdentifier</key>\n <string>{}</string>\n",
plist_escape(bundle_id));
doc += std::format(" <key>CFBundleName</key>\n <string>{}</string>\n",
plist_escape(name));
doc += std::format(" <key>CFBundleShortVersionString</key>\n <string>{}</string>\n",
plist_escape(version));
doc += std::format(" <key>CFBundleVersion</key>\n <string>{}</string>\n",
plist_escape(version));
doc += " <key>CFBundlePackageType</key>\n <string>APPL</string>\n";
if (!min_os_version.empty())
doc += std::format(" <key>{}</key>\n <string>{}</string>\n",
is_ios ? "MinimumOSVersion" : "LSMinimumSystemVersion",
plist_escape(min_os_version));
if (is_ios) {
// `env == "sim"` is the engine's own distinction between the two
// rows (triple.cppm: "the simulator is deliberately not a row of
// its own identity... it has its own SDK"); this key is the one
// place a bundle has to restate it, because `simctl install` and a
// real device's installer both read it to refuse the other kind.
doc += std::format(
" <key>CFBundleSupportedPlatforms</key>\n <array>\n"
" <string>{}</string>\n </array>\n",
is_sim ? "iPhoneSimulator" : "iPhoneOS");
// `[1, 2]`: iPhone and iPad. Nothing here narrows a project to one
// idiom -- that is a project decision (a size class, a storyboard)
// this member has no basis for making.
const auto replaced = [&](std::string_view k) {
return std::ranges::find(replaced_keys, k) != replaced_keys.end();
};
if (!replaced("UIDeviceFamily"))
doc += " <key>UIDeviceFamily</key>\n <array>\n"
" <integer>1</integer>\n <integer>2</integer>\n </array>\n";
if (!replaced("LSRequiresIPhoneOS"))
doc += " <key>LSRequiresIPhoneOS</key>\n <true/>\n";
if (!ios_icon_stems.empty()) {
doc += " <key>CFBundleIcons</key>\n <dict>\n"
" <key>CFBundlePrimaryIcon</key>\n <dict>\n"
" <key>CFBundleIconFiles</key>\n <array>\n";
for (auto const& stem : ios_icon_stems)
doc += std::format(" <string>{}</string>\n", plist_escape(stem));
doc += " </array>\n </dict>\n </dict>\n";
}
} else {
if (std::ranges::find(replaced_keys, std::string_view("NSHighResolutionCapable")) == replaced_keys.end())
doc += " <key>NSHighResolutionCapable</key>\n <true/>\n";
// Not in the letter of this member's key list, but added
// deliberately: without it, an icon file this member went to the
// trouble of copying into `Contents/Resources/` is inert -- nothing
// in the bundle would ever reference it, and Finder would show the
// generic application icon regardless of `options::icon`. Wiring the
// file up once it exists is what makes the option do what its name
// says.
if (!mac_icon_name.empty())
doc += std::format(" <key>CFBundleIconFile</key>\n <string>{}</string>\n",
plist_escape(mac_icon_name));
}
doc += extra_entries;
doc += "</dict>\n</plist>\n";
return doc;
}
inline std::string plist_document(const std::string& executable, const std::string& bundle_id,
const std::string& name, const std::string& version,
bool is_ios, bool is_sim,
const std::string& min_os_version,
const std::string& mac_icon_name,
const std::vector<std::string>& ios_icon_stems) {
return plist_document(executable, bundle_id, name, version, is_ios, is_sim, min_os_version,
std::string(), std::vector<std::string>(), mac_icon_name, ios_icon_stems);
}
inline std::string read_text(const std::string& path) {
std::ifstream in(path, std::ios::binary);
return std::string((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
}
inline std::string resolve_path(const std::string& p) {
if (p.empty() || std::filesystem::path(p).is_absolute()) return p;
return (std::filesystem::path(mcpp::manifest_dir()) / p).lexically_normal().string();
}
// The keys `plist_document` derives, which a project's plist may not restate,
// and the keys it only defaults, which a project's plist replaces.
inline const std::vector<std::string>& derived_plist_keys() {
static const std::vector<std::string> v = {
"CFBundleExecutable", "CFBundleIdentifier", "CFBundleName", "CFBundleShortVersionString",
"CFBundleVersion", "CFBundlePackageType", "LSMinimumSystemVersion", "MinimumOSVersion",
"CFBundleSupportedPlatforms", "CFBundleIconFile", "CFBundleIcons"};
return v;
}
inline const std::vector<std::string>& defaulted_plist_keys() {
static const std::vector<std::string> v = {"UIDeviceFamily", "LSRequiresIPhoneOS", "NSHighResolutionCapable"};
return v;
}
// The top-level `<dict>` of a property list, or null. A document whose root is
// `<dict>` itself is accepted as well as a full `<plist>`.
inline const mcpp::plugins::xml::node* top_dict(const mcpp::plugins::xml::node& root) {
if (root.name == "dict") return &root;
if (root.name != "plist") return nullptr;
for (auto const& c : root.children) if (c.name == "dict") return &c;
return nullptr;
}
// The value element after each `<key>` of a `<dict>`, as (key, value) pairs.
inline bool dict_entries(const mcpp::plugins::xml::node& dict,
std::vector<std::pair<std::string, const mcpp::plugins::xml::node*>>& out,
std::string& error) {
namespace xml = mcpp::plugins::xml;
for (std::size_t i = 0; i < dict.children.size(); ++i) {
const auto& k = dict.children[i];
if (k.name.empty()) continue;
if (k.name != "key") { error = "<" + k.name + "> appears where a <key> is expected"; return false; }
const std::string key = k.children.size() == 1 && k.children.front().name.empty()
? xml::trim_copy(k.children.front().text) : std::string();
std::size_t j = i + 1;
while (j < dict.children.size() && dict.children[j].name.empty()) ++j;
if (key.empty() || j >= dict.children.size()) { error = "a <key> without a value"; return false; }
out.emplace_back(key, &dict.children[j]);
i = j;
}
return true;
}
// One Info.plist entry a fragment states: its key and the plist text of the
// `<key>` and its value, as `plist_document` inserts it.
struct plist_entry {
std::string key;
std::string text;
};
// Reads one plist fragment's entries. `who` is how a diagnostic names the
// fragment (`options::info_plist`, or a package's [package.metadata.dist-apple]).
inline bool read_plist_entries(const std::string& path, const std::string& who,
std::vector<plist_entry>& out, std::string& message) {
namespace xml = mcpp::plugins::xml;
xml::node root;
std::string err;
if (!xml::parse(read_text(path), root, err)) {
message = std::format("mcpp.dist.apple: {} ({}) cannot be read: {}", who, path, err);
return false;
}
const xml::node* dict = top_dict(root);
if (!dict) {
message = std::format("mcpp.dist.apple: {} ({}) has no top-level <dict>.", who, path);
return false;
}
std::vector<std::pair<std::string, const xml::node*>> pairs;
if (!dict_entries(*dict, pairs, err)) {
message = std::format("mcpp.dist.apple: {} ({}): {}", who, path, err);
return false;
}
std::vector<std::string> seen;
for (auto const& [key, value] : pairs) {
if (std::ranges::find(derived_plist_keys(), key) != derived_plist_keys().end()) {
message = std::format(
"mcpp.dist.apple: {} ({}) sets {}, which this member derives "
"from its options and the engine; set the option instead.", who, path, key);
return false;
}
if (std::ranges::find(seen, key) != seen.end()) {
message = std::format("mcpp.dist.apple: {} ({}) sets {} twice.", who, path, key);
return false;
}
seen.push_back(key);
plist_entry e;
e.key = key;
e.text = " <key>" + key + "</key>\n";
xml::write(*value, e.text, 1);
out.push_back(std::move(e));
}
return true;
}
// A provisioning profile is a CMS-signed property list. The plist is stored in
// the signed content as-is, so it is read from between its `<?xml` and
// `</plist>` rather than through `security cms -D`, which exists only on macOS
// and would make a plan made elsewhere unable to check anything. The signature
// is the device's to verify, not this member's.
inline bool read_profile(const std::string& path, const std::string& bundleId,
std::string& entitlementsPlist, std::string& message) {
namespace xml = mcpp::plugins::xml;
const std::string bytes = read_text(path);
const auto b = bytes.find("<?xml");
const auto e = bytes.find("</plist>");
if (b == std::string::npos || e == std::string::npos || e < b) {
message = std::format(
"mcpp.dist.apple: `options::provisioning_profile` ({}) carries no property list, so "
"it is not a provisioning profile.", path);
return false;
}
xml::node root;
std::string err;
if (!xml::parse(std::string_view(bytes).substr(b, e + 8 - b), root, err)) {
message = std::format("mcpp.dist.apple: the property list in {} cannot be read: {}", path, err);
return false;
}
const xml::node* dict = top_dict(root);
std::vector<std::pair<std::string, const xml::node*>> pairs;
if (!dict || !dict_entries(*dict, pairs, err)) {
message = std::format("mcpp.dist.apple: the property list in {} has no readable top-level <dict>.", path);
return false;
}
const xml::node* entitlements = nullptr;
for (auto const& [key, value] : pairs) if (key == "Entitlements" && value->name == "dict") entitlements = value;
std::vector<std::pair<std::string, const xml::node*>> granted;
if (!entitlements || !dict_entries(*entitlements, granted, err)) {
message = std::format("mcpp.dist.apple: the provisioning profile {} states no Entitlements dictionary.", path);
return false;
}
std::string appIdentifier;
for (auto const& [key, value] : granted)
if (key == "application-identifier" && value->children.size() == 1 && value->children.front().name.empty())
appIdentifier = xml::trim_copy(value->children.front().text);
const auto dot = appIdentifier.find('.');
const std::string pattern = dot == std::string::npos ? std::string() : appIdentifier.substr(dot + 1);
const bool covers = !pattern.empty() &&
(pattern == bundleId || pattern == "*" ||
(pattern.ends_with(".*") && bundleId.starts_with(pattern.substr(0, pattern.size() - 1))));
if (!covers) {
message = std::format(
"mcpp.dist.apple: the provisioning profile {} is for the application identifier '{}', "
"which does not cover the bundle identifier '{}'. Set `options::bundle_id` to match, "
"or use the profile made for it.", path,
appIdentifier.empty() ? std::string("(none)") : appIdentifier, bundleId);
return false;
}
// A WILDCARD PROFILE GRANTS `<team>.*`, AND THE SIGNATURE STATES THE BUNDLE.
// Xcode signs with the application identifier the bundle is, which the
// wildcard covers, rather than with the wildcard itself; the other
// entitlements are the profile's as it states them.
xml::node signedWith = *entitlements;
if (pattern != bundleId) {
const std::string exact = appIdentifier.substr(0, dot + 1) + bundleId;
for (std::size_t k = 0; k + 1 < signedWith.children.size(); ++k) {
auto const& key = signedWith.children[k];
auto& value = signedWith.children[k + 1];
if (key.name == "key" && key.children.size() == 1 &&
xml::trim_copy(key.children.front().text) == "application-identifier" &&
value.name == "string" && value.children.size() == 1 && value.children.front().name.empty())
value.children.front().text = exact;
}
}
entitlementsPlist = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" "
"\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
"<plist version=\"1.0\">\n";
xml::write(signedWith, entitlementsPlist, 0);
entitlementsPlist += "</plist>\n";
return true;
}
// ─── Plan ──────────────────────────────────────────────────────────────────
inline plan plan_for(options opt = {}) {
plan p;
// NOT THIS PASS. Every ordinary build lands here, and the empty
// `pack_format()` is what says so -- see `generate` for why the
// DECLARATION must not be gated the same way.
const std::string requested = mcpp::pack_format();
if (requested != "app" && requested != "dmg") {
p.reason = requested.empty()
? "this build is not packaging"
: std::format("--format {} was requested, not app or dmg", requested);
return p;
}
const bool dmg = requested == "dmg";
// macOS or iOS only, and this is a refusal rather than a silent skip: a
// user who typed `--format app` on Linux asked for something that does
// not exist there, and the engine has already accepted the value
// because the graph declared it.
const std::string os = mcpp::target_os();
const bool isIos = (os == "ios");
if (os != "macos" && !isIos) {
return refuse(p, "not a macOS or iOS target", std::format(
"mcpp.dist.apple: a .app bundle is a macOS or iOS format, and "
"this build targets '{}'.\n"
" use: --format tar, or build for a macOS or iOS target",
os.empty() ? "unknown" : os));
}
if (dmg && isIos) {
return refuse(p, "a disk image on an iOS target",
"mcpp.dist.apple: a .dmg is a macOS disk image, and this build "
"targets iOS. Use --format app for an iOS bundle.");
}
// `aarch64-ios-sim` and `aarch64-ios` share one OS and diverge only in
// `env` (triple.cppm's own words: "the simulator is deliberately not a
// row [of its own identity]"). Every place this member reads the
// simulator/device split reads it from here, once.
const bool isSim = isIos && (std::string(mcpp::target_env()) == "sim");
// THE STAGED TREE IS OPTIONAL, AND THAT IS THE WHOLE FINDING.
//
// This required it, and on macOS it cannot exist: `mcpp pack`'s built-in
// closure walk refuses a Mach-O program, because it uses
// `LD_TRACE_LOADED_OBJECTS` and dyld answers that by RUNNING the program.
// mcpp 2026.9.11.2 made staging a service rather than a precondition, so
// the dispatch now reaches this member -- and the member then refused for
// the same underlying reason, one layer up, with
//
// error: no action claimed --format 'app'
//
// from the engine, because a member's stderr on a successful build is
// discarded. A refusal nobody can read.
//
// A `.app` needs ONE program, not a tree. `${mcpp.target_file:<name>}` is
// what names it -- the same placeholder `dist/wix.cppm` uses for exactly
// this reason, and what section 6 of the design record recommends for a
// member that packages a named target. The staged tree is still preferred
// when it exists, because a `--mode vendored` tree carries the program's
// dependencies beside it and a bundle should keep them; without one the
// bundle carries the program alone, which is correct for a self-contained
// Mach-O and is what the platform's own default produces.
const std::string stage = mcpp::pack_stage_dir();
const std::string target = target_for(opt);
if (target.empty()) {
return refuse(p, "no target", "mcpp.dist.apple: no target to bundle. Set "
"`options::target` to the program target's name.");
}
const std::string launcher = stage.empty()
? std::format("${{mcpp.target_file:{}}}", target)
: launcher_in(stage, target);
if (launcher.empty()) {
return refuse(p, "no launcher in the staged tree", std::format(
"mcpp.dist.apple: the staged tree at {0} carries no launcher for "
"target '{1}'.\n"
" expected one of: {0}/bin/{1}, {0}/{1}, {0}/run.sh",
stage, target));
}
// CFBundleExecutable is a bare filename (see the note above). With no
// staged tree the launcher is a PLACEHOLDER the engine expands later, so
// its basename cannot be taken from the string -- the target's own name is
// what the expansion will produce.
const std::string executableName = stage.empty()
? target : bundle_executable_name(launcher);
// `options::icon` IS RESOLVED AGAINST THE MANIFEST DIRECTORY HERE, ONCE,
// BEFORE ANY VALIDATION OR ACTION ARGV USES IT -- ON BOTH ROWS.
//
// This program's own cwd is the manifest directory when mcpp runs it (the
// usual case a project author sees, and why a bare relative path like
// `"ios-icons"` validates below without complaint). But the ACTIONS this
// member declares -- the `ditto` calls in the layout and icon steps -- are
// graph edges ninja runs later, with the BUILD directory as their cwd, not
// the manifest directory. A relative `options::icon` therefore reached
// `ditto` as a path that does not exist from where `ditto` was standing:
//
// ditto ios-icons .../IosAppConsumer.app
// ditto: Cannot get the real path for source 'ios-icons'
//
// failing inside the graph, on a host with no way to run this member
// again to explain why. Resolving here, against `mcpp::manifest_dir()`,
// makes every later use -- the validation immediately below and the
// `icon.argv` this function builds further down -- see the same absolute
// path regardless of which directory the thing reading it is standing in.
// An already-absolute `options::icon` is left alone.
if (!opt.icon.empty()) {
std::filesystem::path iconPath(opt.icon);
if (!iconPath.is_absolute())
opt.icon = (std::filesystem::path(mcpp::manifest_dir()) / iconPath).string();
}
// macOS: `options::icon` is a FILE. iOS: it is a DIRECTORY of flat PNGs
// (see the `options::icon` comment and the header's icon paragraph) --
// two different validations of the same field, because the two
// platforms' icon conventions are not the same shape and this member
// does not invent a third field to hold the distinction. Both refusals
// name `options::icon` and the (now-resolved) path, so a project sees
// exactly what this member read rather than a bare relative name it typed.
std::vector<std::string> iosIconStems;
if (!opt.icon.empty()) {
if (isIos) {
std::error_code ec;
if (!std::filesystem::is_directory(opt.icon, ec)) {
return refuse(p, "icon is not a directory", std::format(
"mcpp.dist.apple: `options::icon` ({}) is not a "
"directory. Set it to a directory of flat PNGs "
"(one per size Apple's Home Screen and Settings need); "
"this member lists their stems under `CFBundleIcons` "
"and does not generate sizes itself.", opt.icon));
}
for (auto const& e : std::filesystem::directory_iterator(opt.icon, ec)) {
if (ec) break;
if (e.is_regular_file(ec) && e.path().extension() == ".png")
iosIconStems.push_back(e.path().stem().string());
}
std::sort(iosIconStems.begin(), iosIconStems.end());
if (iosIconStems.empty()) {
return refuse(p, "icon directory carries no PNGs", std::format(
"mcpp.dist.apple: `options::icon` ({}) carries no "
"*.png files.", opt.icon));
}
} else if (!is_file(opt.icon)) {
return refuse(p, "icon not found", std::format(
"mcpp.dist.apple: `options::icon` ({}) was not found",
opt.icon));
}
}
// Resolved against the manifest for the reason `options::icon` is above.
opt.entitlements = resolve_path(opt.entitlements);
opt.info_plist = resolve_path(opt.info_plist);
opt.provisioning_profile = resolve_path(opt.provisioning_profile);
std::string extraEntries;
std::vector<std::string> replacedKeys;
std::vector<std::string> contributedDefaults;
// The application's own entries, read first so that their refusals come
// before any contribution's; they are applied last.
std::vector<plist_entry> ownEntries;
if (!opt.info_plist.empty()) {
if (!is_file(opt.info_plist)) {
return refuse(p, "info_plist not found", std::format(
"mcpp.dist.apple: `options::info_plist` ({}) was not found", opt.info_plist));
}
mcpp::rerun_if_changed(opt.info_plist.c_str());
std::string message;
if (!read_plist_entries(opt.info_plist, "`options::info_plist`", ownEntries, message))
return refuse(p, "unusable info_plist", message);
for (auto const& e : ownEntries)
if (std::ranges::find(defaulted_plist_keys(), e.key) != defaulted_plist_keys().end())
replacedKeys.push_back(e.key);
}
// The entries the graph's packages contribute, in the graph's order
// (dependencies first), each replacing an earlier contribution of the same
// key; then the application's own, replacing any contribution.
std::vector<plist_entry> merged;
const auto apply_entries = [&](const std::vector<plist_entry>& list) {