Compare commits
1418 Commits
v1.6.3
...
cb93168516
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb93168516 | ||
|
|
4a688f20fc | ||
|
|
d37ba80ca1 | ||
|
|
a3190fb016 | ||
|
|
aec39c0ccd | ||
|
|
c82662a72b | ||
|
|
a6315442fb | ||
|
|
97d8da1eab | ||
|
|
4975ea4fcd | ||
|
|
88f7122d8d | ||
|
|
50482a9dc4 | ||
|
|
af04388aea | ||
|
|
7553e25bbf | ||
|
|
7c01ba0db6 | ||
|
|
e55351dbb0 | ||
|
|
97ee0b2dac | ||
|
|
8fc38300d5 | ||
|
|
5460fdfec5 | ||
|
|
ded65e30d6 | ||
|
|
b0fab7550f | ||
|
|
10154954f0 | ||
|
|
59892247a6 | ||
|
|
f350c9fa49 | ||
|
|
32fb3d706c | ||
|
|
232dec2c36 | ||
|
|
e9772afbf6 | ||
|
|
014d7b8346 | ||
|
|
8d90def5d8 | ||
|
|
e891b7ec93 | ||
|
|
e5dbf2b677 | ||
|
|
4ff35e36e4 | ||
|
|
3927cbd438 | ||
|
|
23d5123d07 | ||
|
|
f24b728223 | ||
|
|
d93df1ce9c | ||
|
|
016ef38c92 | ||
|
|
267e7bd688 | ||
|
|
42091a98be | ||
|
|
6d4cbf1c59 | ||
|
|
747a9c7b5a | ||
|
|
eed895f145 | ||
|
|
8fe551d849 | ||
|
|
d670618cbe | ||
|
|
f16651040f | ||
|
|
e028b768f4 | ||
|
|
a6be43da53 | ||
|
|
c807154873 | ||
|
|
1103a7f0b4 | ||
|
|
8a63725113 | ||
|
|
4a73efe680 | ||
|
|
be4d62fb1e | ||
|
|
7683447eea | ||
|
|
d18422af00 | ||
|
|
0062374d32 | ||
|
|
d45803a7cc | ||
|
|
661fda4988 | ||
|
|
83ed17c443 | ||
|
|
096bb85bc1 | ||
|
|
786f06800f | ||
|
|
146b69944a | ||
|
|
37cc2cfca6 | ||
|
|
31fe7fed8c | ||
|
|
dbc20391e5 | ||
|
|
aca8193f1c | ||
|
|
fabe093b1d | ||
|
|
277b5b0bd4 | ||
|
|
e1550811eb | ||
|
|
255db2bac0 | ||
|
|
30c03aad42 | ||
|
|
d70c916222 | ||
|
|
7c038568f8 | ||
|
|
6d511de31d | ||
|
|
8730ae42c6 | ||
|
|
7d7a44bc61 | ||
|
|
639753149f | ||
|
|
cc5aeaf099 | ||
|
|
1d642e2ffa | ||
|
|
decf21f177 | ||
|
|
b2f42160f9 | ||
|
|
4f8a70d31e | ||
|
|
bedda8c6a4 | ||
|
|
d042267ff9 | ||
|
|
4cc534c89e | ||
|
|
606140a7be | ||
|
|
55761029a3 | ||
|
|
4e6756d4d5 | ||
|
|
c64b0c99de | ||
|
|
e19b147962 | ||
|
|
85427ecbc6 | ||
|
|
ecf5e2d770 | ||
|
|
2f2df9ca43 | ||
|
|
31520354bd | ||
|
|
25f13faa67 | ||
|
|
dcaddcc219 | ||
|
|
fa1757ff1d | ||
|
|
f0cb86aff4 | ||
|
|
7a34d7ac67 | ||
|
|
cfd9247fdd | ||
|
|
15ffe766d3 | ||
|
|
a0ae0cb2b1 | ||
|
|
dc53f19947 | ||
|
|
baf3594a8e | ||
|
|
1d4666e79e | ||
|
|
b4df06f742 | ||
|
|
d41a6b17d2 | ||
|
|
d98e0dde72 | ||
|
|
f23a7e9c82 | ||
|
|
7ed45c7cb1 | ||
|
|
c319524df6 | ||
|
|
724e8db980 | ||
|
|
669d33183e | ||
|
|
a12b6cd62b | ||
|
|
685d956775 | ||
|
|
00ed6e61be | ||
|
|
7057d59992 | ||
|
|
6d308487c1 | ||
|
|
6c45eb096f | ||
|
|
4b6ffa5d4a | ||
|
|
728b12d004 | ||
|
|
97160569ba | ||
|
|
1b754e137a | ||
|
|
4176b8c83c | ||
|
|
479f877dbf | ||
|
|
4ff643805b | ||
|
|
0e3c3dddfe | ||
|
|
7415fe6204 | ||
|
|
c128c9268b | ||
|
|
131a0289a2 | ||
|
|
116f045e51 | ||
|
|
c68efd2b44 | ||
|
|
e519389780 | ||
|
|
e6135c6518 | ||
|
|
3d444f301d | ||
|
|
7adfd3d3e8 | ||
|
|
591913743e | ||
|
|
77d95fe278 | ||
|
|
7ec6324b62 | ||
|
|
0cd62d371a | ||
|
|
9771f6bb9a | ||
|
|
e553efbad5 | ||
|
|
a4dfd28a3c | ||
|
|
291d7fd8d9 | ||
|
|
a7496e08e3 | ||
|
|
2f3be07b5d | ||
|
|
b57d279670 | ||
|
|
cbde3e6495 | ||
|
|
117f31e158 | ||
|
|
420efcbb11 | ||
|
|
dc29b65bd5 | ||
|
|
1e64f50ab9 | ||
|
|
ae8e9a3506 | ||
|
|
3785891923 | ||
|
|
e85db67ac7 | ||
|
|
39d0835f5b | ||
|
|
c8127fb3b9 | ||
|
|
729fca9100 | ||
|
|
c6703d66c1 | ||
|
|
2197161d55 | ||
|
|
cfaf79f878 | ||
|
|
2bea980d1f | ||
|
|
f11e27c712 | ||
|
|
b316e84f0d | ||
|
|
4668f8499b | ||
|
|
f9aedaba04 | ||
|
|
8cb3de9ab5 | ||
|
|
ca57d58219 | ||
|
|
6a56fbd206 | ||
|
|
cec3825de0 | ||
|
|
b3e99d1ae3 | ||
|
|
7243d79646 | ||
|
|
e50da6603c | ||
|
|
136f07e4b9 | ||
|
|
f4d367b857 | ||
|
|
3e96583525 | ||
|
|
84bb1f7c08 | ||
|
|
a5aec978bb | ||
|
|
d677292bd3 | ||
|
|
722f67d3c7 | ||
|
|
07e0aea24f | ||
|
|
673f2ba693 | ||
|
|
0070519736 | ||
|
|
359b38609b | ||
|
|
348a24d885 | ||
|
|
ebdd0d4cb4 | ||
|
|
614ab4cd33 | ||
|
|
ca38008328 | ||
|
|
e5713fa3a9 | ||
|
|
35e3cf1e14 | ||
|
|
ca8c394ae0 | ||
|
|
5323fdadcf | ||
|
|
e43bff5f8f | ||
|
|
4659d617f8 | ||
|
|
87fe360927 | ||
|
|
71212130c5 | ||
|
|
6370105c85 | ||
|
|
15f4ae7b5a | ||
|
|
08e11eece4 | ||
|
|
1a43ad4aa3 | ||
|
|
a5ec12e3df | ||
|
|
c0400446bc | ||
|
|
13c3d0c1e9 | ||
|
|
92edd4b800 | ||
|
|
eb0e1326b9 | ||
|
|
a8fd1a3e83 | ||
|
|
533655c96b | ||
|
|
a322f9b5e9 | ||
|
|
4805ed8d3b | ||
|
|
caa38cfcae | ||
|
|
77833c6ffc | ||
|
|
61086a681a | ||
|
|
cf81d2876c | ||
|
|
e5144d112a | ||
|
|
bf766c1b84 | ||
|
|
e608211f32 | ||
|
|
ef9135d7ee | ||
|
|
8b2c92d94c | ||
|
|
ac486a4723 | ||
|
|
c3847276f7 | ||
|
|
e43e962bcc | ||
|
|
0245f6072a | ||
|
|
ac63314a91 | ||
|
|
803eb615cd | ||
|
|
b218437388 | ||
|
|
df2ffb1235 | ||
|
|
7ed8c20dfc | ||
|
|
203a7e6f11 | ||
|
|
c6f843ebc3 | ||
|
|
13d924abf6 | ||
|
|
1641f6e93f | ||
|
|
c0a81edf6b | ||
|
|
e9391b7a21 | ||
|
|
8935ba08b4 | ||
|
|
eb539cd7f6 | ||
|
|
172481fee5 | ||
|
|
28de1953c4 | ||
|
|
29ed2afb3d | ||
|
|
332f56324c | ||
|
|
341e498b3f | ||
|
|
1a0a7eb9d7 | ||
|
|
5c55f45c84 | ||
|
|
c62cc3dabd | ||
|
|
acb8af5645 | ||
|
|
0da743e9a6 | ||
|
|
abd3735ae4 | ||
|
|
1eba9b60cb | ||
|
|
3e74e4ae5d | ||
|
|
e8798499d3 | ||
|
|
dc4a543b3b | ||
|
|
1649bb73d9 | ||
|
|
337349c324 | ||
|
|
d81c59ecf9 | ||
|
|
e62d9023f8 | ||
|
|
c5def98e87 | ||
|
|
48d691204b | ||
|
|
e2836fcd70 | ||
|
|
a8395ebcdc | ||
|
|
55f4aa7deb | ||
|
|
61989e7d40 | ||
|
|
d13e469cf2 | ||
|
|
6ed5acdb40 | ||
|
|
ba825d4218 | ||
|
|
720a397dd4 | ||
|
|
e29d9f7904 | ||
|
|
f69b293004 | ||
|
|
4e10527f03 | ||
|
|
46871f63ed | ||
|
|
bb8a1b57cd | ||
|
|
9283ce3289 | ||
|
|
efbcafc7cc | ||
|
|
c721fa01f9 | ||
|
|
bd5b011a62 | ||
|
|
b3382df216 | ||
|
|
5db45565f3 | ||
|
|
4e71e5d879 | ||
|
|
a967a7aaad | ||
|
|
74749b6502 | ||
|
|
bf8a42d490 | ||
|
|
a2f37c8895 | ||
|
|
1ea2f0e5e6 | ||
|
|
ee03d9fa51 | ||
|
|
51f2588d3c | ||
|
|
5416deddbe | ||
|
|
3525fb4afa | ||
|
|
82c04cd423 | ||
|
|
1666c40bd8 | ||
|
|
7ae10c6022 | ||
|
|
a547d2bc8a | ||
|
|
fba5ca4e12 | ||
|
|
2e01116f1f | ||
|
|
2e247f1773 | ||
|
|
e21e116535 | ||
|
|
8a77090586 | ||
|
|
d1be3f1914 | ||
|
|
198fb45c9d | ||
|
|
066d3e11a2 | ||
|
|
68470b61c5 | ||
|
|
6c5cf2ef06 | ||
|
|
73c86661be | ||
|
|
ff4a000406 | ||
|
|
c69c8c69fa | ||
|
|
e165273554 | ||
|
|
e3d08ebf16 | ||
|
|
3f959f111c | ||
|
|
950e54a04b | ||
|
|
a22fb3fd2f | ||
|
|
7971a896c6 | ||
|
|
210d235345 | ||
|
|
b39840e344 | ||
|
|
8b9e473ace | ||
|
|
178145f57e | ||
|
|
7f18e5a678 | ||
|
|
0b43b6d152 | ||
|
|
397bd21ff6 | ||
|
|
a642553b43 | ||
|
|
5e753855a3 | ||
|
|
d6ae65420d | ||
|
|
2c29e3f902 | ||
|
|
e01e31f3d5 | ||
|
|
caf491aa00 | ||
|
|
b80e0757e8 | ||
|
|
1f1b7ff083 | ||
|
|
f2e3a31520 | ||
|
|
79794f7fd5 | ||
|
|
157bfed965 | ||
|
|
e3d0dac09a | ||
|
|
e1d8887907 | ||
|
|
7e45d4fa33 | ||
|
|
015863b4fc | ||
|
|
a7b84bbc20 | ||
|
|
dded866025 | ||
|
|
88519ff5e8 | ||
|
|
3f2f93d2b5 | ||
|
|
c1910f49eb | ||
|
|
571a65a46a | ||
|
|
2c07d14a00 | ||
|
|
a2a199d64e | ||
|
|
893c628e80 | ||
|
|
523c543690 | ||
|
|
ebf055dc7d | ||
|
|
daa5ba1a9f | ||
|
|
54a895c11a | ||
|
|
2eeaeca8f4 | ||
|
|
7af0992a2b | ||
|
|
cf0a2001f0 | ||
|
|
cfe2111844 | ||
|
|
09e39d1ead | ||
|
|
dd8d6d447f | ||
|
|
992b6b7262 | ||
|
|
1f9326b452 | ||
|
|
36f64f6c0a | ||
|
|
901b9ae337 | ||
|
|
a173455e2d | ||
|
|
adee94819a | ||
|
|
c41d25c19b | ||
|
|
1203c9f5f2 | ||
|
|
1b85f6532b | ||
|
|
f080379994 | ||
|
|
a9a698b704 | ||
|
|
f9f56c4d66 | ||
|
|
fd03b33e9b | ||
|
|
2737384147 | ||
|
|
e005846324 | ||
|
|
bfed227940 | ||
|
|
8cfbe309ef | ||
|
|
d0eeefbc34 | ||
|
|
d716d88d33 | ||
|
|
5b69eb7bef | ||
|
|
21ab603756 | ||
|
|
7af8f5c817 | ||
|
|
460389f9c1 | ||
|
|
9b50ed83d3 | ||
|
|
89ec42ca87 | ||
|
|
0ab47360ff | ||
|
|
93ca83528b | ||
|
|
e39b9fe309 | ||
|
|
4cb85f3d59 | ||
|
|
7dc37d82e3 | ||
|
|
b7b5f50e69 | ||
|
|
7642c2f948 | ||
|
|
6cf8d4b48a | ||
|
|
d96056762f | ||
|
|
8f20b5bb8d | ||
|
|
7d2a6a9189 | ||
|
|
f7efb6569e | ||
|
|
4bc76fe93b | ||
|
|
59a5495ec0 | ||
|
|
7353a1f28b | ||
|
|
b757410044 | ||
|
|
5058658b66 | ||
|
|
f542a52bda | ||
|
|
976f4e1041 | ||
|
|
77e223e541 | ||
|
|
82cb14df6c | ||
|
|
6c559325b9 | ||
|
|
e662ba64fa | ||
|
|
49705a71f9 | ||
|
|
78081947c4 | ||
|
|
182dfb3c75 | ||
|
|
90c6c64e71 | ||
|
|
6c60c2092c | ||
|
|
90a1f4dd83 | ||
|
|
9597c7deb6 | ||
|
|
d045c0ed4d | ||
|
|
3ea24fdfea | ||
|
|
c19c68a2cf | ||
|
|
46ccbe5aad | ||
|
|
7ac485def0 | ||
|
|
7f60048b0c | ||
|
|
1d030a9550 | ||
|
|
5b8ca13565 | ||
|
|
ad431fb4b7 | ||
|
|
26b6c8d034 | ||
|
|
8ebb0d0f35 | ||
|
|
17018ea20f | ||
|
|
67f3c3a561 | ||
|
|
a2812c0528 | ||
|
|
a96fb13372 | ||
|
|
76070ffaa1 | ||
|
|
cec00769a9 | ||
|
|
a36043840f | ||
|
|
c0de04183c | ||
|
|
da949ec85f | ||
|
|
5a369574cc | ||
|
|
6650d3b73f | ||
|
|
e50cce0d5e | ||
|
|
148250f29a | ||
|
|
d6fc92cf5f | ||
|
|
99b16b83b6 | ||
|
|
792ca96ff3 | ||
|
|
3494c5b33b | ||
|
|
49502ce1ef | ||
|
|
eafeb97fa6 | ||
|
|
259870c92e | ||
|
|
e1cbec3864 | ||
|
|
779a82f3d4 | ||
|
|
d9d9a9a156 | ||
|
|
a7b31ec7a2 | ||
|
|
63a3203e50 | ||
|
|
e27c5f0b87 | ||
|
|
e298b440e8 | ||
|
|
03f46515ef | ||
|
|
81542d253f | ||
|
|
8bf3aef06c | ||
|
|
8a21c66927 | ||
|
|
44ade05e53 | ||
|
|
fd402f924f | ||
|
|
1f83ea00d3 | ||
|
|
d07b3e6c3a | ||
|
|
c0e780cbbd | ||
|
|
c86e00413b | ||
|
|
d2ed5bff57 | ||
|
|
07ed25ab54 | ||
|
|
2568f41e20 | ||
|
|
8ba28d0cd4 | ||
|
|
aa655fc5a3 | ||
|
|
d3d609550e | ||
|
|
fc5355345e | ||
|
|
aa1ed04bce | ||
|
|
2899e3ea5f | ||
|
|
6ee90f6c2a | ||
|
|
2f603778d6 | ||
|
|
ac62612a18 | ||
|
|
b8b32d501c | ||
|
|
edcdf94383 | ||
|
|
bce824254b | ||
|
|
846285eb1f | ||
|
|
74a231242e | ||
|
|
1c02da8806 | ||
|
|
f85dcdcd4a | ||
|
|
f477115003 | ||
|
|
79fc75edbd | ||
|
|
6d7d06a85e | ||
|
|
afb393b80b | ||
|
|
cfdc1aa82c | ||
|
|
2466c4d5c9 | ||
|
|
673eff4a51 | ||
|
|
3c73418fc3 | ||
|
|
b72b46b864 | ||
|
|
69a01a3262 | ||
|
|
becc626027 | ||
|
|
90fbb28b54 | ||
|
|
4688e9b927 | ||
|
|
7bcd0ea748 | ||
|
|
6a520b3410 | ||
|
|
f2ab214f27 | ||
|
|
de601cfacb | ||
|
|
b7a04d59b4 | ||
|
|
fec02cd80a | ||
|
|
9b1d65b571 | ||
|
|
be640930ce | ||
|
|
31aeec6b38 | ||
|
|
d7aa3b405d | ||
|
|
5d27397f03 | ||
|
|
bea54fa748 | ||
|
|
3391058475 | ||
|
|
55aa893b21 | ||
|
|
2ebd234ec8 | ||
|
|
2b3b60819e | ||
|
|
b0e43b8b97 | ||
|
|
4514fe1c2c | ||
|
|
1fbec33719 | ||
|
|
74b6fb6ec0 | ||
|
|
703db062e6 | ||
|
|
29627504cc | ||
|
|
1992ecd3a2 | ||
|
|
36ac3af7dc | ||
|
|
786bf4ac63 | ||
|
|
86d7afb95d | ||
|
|
81af866268 | ||
|
|
abc7b8d933 | ||
|
|
9ac26a99a8 | ||
|
|
80030acb87 | ||
|
|
0c958dad19 | ||
|
|
9ea94912d4 | ||
|
|
36743c0cf4 | ||
|
|
870ef0c47f | ||
|
|
5c808ec59e | ||
|
|
20b424c97c | ||
|
|
1b8daa59c0 | ||
|
|
71eb77cfda | ||
|
|
5cb40de113 | ||
|
|
9716ee6152 | ||
|
|
cf09843538 | ||
|
|
d625b381d9 | ||
|
|
05332ca122 | ||
|
|
be31611cb7 | ||
|
|
3773a051ab | ||
|
|
142b9c6810 | ||
|
|
cccbe3a80b | ||
|
|
0ad9ceaa95 | ||
|
|
8946ab8a65 | ||
|
|
a981201016 | ||
|
|
5da80863b9 | ||
|
|
850b6890e2 | ||
|
|
e370034e0b | ||
|
|
8add1d0f4a | ||
|
|
ba94886ba9 | ||
|
|
dddb9c5ddb | ||
|
|
389af4d5e6 | ||
|
|
aa9fda7a97 | ||
|
|
9f7f73f835 | ||
|
|
947b31881f | ||
|
|
7e232cafbe | ||
|
|
91ea34110b | ||
|
|
47b69b40aa | ||
|
|
99a3746e1a | ||
|
|
6ba642335b | ||
|
|
869b2dc92a | ||
|
|
f692da047a | ||
|
|
f352aaf9c5 | ||
|
|
299b5ca04c | ||
|
|
d83891d794 | ||
|
|
1b20f17052 | ||
|
|
583de6c0ec | ||
|
|
140193c040 | ||
|
|
50076f6e96 | ||
|
|
a53d45b1dc | ||
|
|
65562602af | ||
|
|
c20ed78cec | ||
|
|
2fa9dbb859 | ||
|
|
edf3281eee | ||
|
|
b89d086e79 | ||
|
|
67271c479c | ||
|
|
7977267107 | ||
|
|
a49e16f591 | ||
|
|
57059967c6 | ||
|
|
c15459e313 | ||
|
|
86ec59d204 | ||
|
|
6fc9ad010d | ||
|
|
932e66deca | ||
|
|
59a72ad096 | ||
|
|
d7384cfae9 | ||
|
|
e33a40ecb1 | ||
|
|
21b2a869a1 | ||
|
|
34c503da4b | ||
|
|
cd42cc827f | ||
|
|
83d8d2e38a | ||
|
|
c3820136a6 | ||
|
|
89347ffffa | ||
|
|
98125e93aa | ||
|
|
2aebaf80e0 | ||
|
|
1d19a31a86 | ||
|
|
ac8f82c30a | ||
|
|
b03b7bfe68 | ||
|
|
f9911d285d | ||
|
|
20cf2326c6 | ||
|
|
bff9eef2dd | ||
|
|
45df1c10cb | ||
|
|
61b2a9bb82 | ||
|
|
df43561494 | ||
|
|
4551451b57 | ||
|
|
727ab0b554 | ||
|
|
d53e36633d | ||
|
|
9ddc27f6ca | ||
|
|
1ece687e37 | ||
|
|
2ccf01cf9c | ||
|
|
c19b541739 | ||
|
|
27ca2f285b | ||
|
|
93b6da4855 | ||
|
|
6adf55b4b6 | ||
|
|
bca5c4e9a4 | ||
|
|
5e48e36ce2 | ||
|
|
4da8316c12 | ||
|
|
81c3825c92 | ||
|
|
3df1297b1c | ||
|
|
d89a15f74b | ||
|
|
55ccb723e5 | ||
|
|
6c0cfdf99e | ||
|
|
9a6272f8e0 | ||
|
|
07b1750a9c | ||
|
|
9cf61bd4df | ||
|
|
e34f3ce201 | ||
|
|
5d5da9e910 | ||
|
|
a341a15a5e | ||
|
|
58b6c0d6ac | ||
|
|
297a71bf91 | ||
|
|
d657ffc8ca | ||
|
|
a1865b6725 | ||
|
|
fa01fa708e | ||
|
|
80fc8e7a1e | ||
|
|
891609b64e | ||
|
|
021b49c436 | ||
|
|
f39ebfb905 | ||
|
|
c4117c0ac9 | ||
|
|
00423bec08 | ||
|
|
46bd977371 | ||
|
|
4410775aec | ||
|
|
b760418252 | ||
|
|
a7b28d5027 | ||
|
|
f83303a6b7 | ||
|
|
76ef018638 | ||
|
|
2aaa7761fc | ||
|
|
ea2600aba9 | ||
|
|
3f6688523a | ||
|
|
641c716d57 | ||
|
|
693969dc28 | ||
|
|
fd8dd20c1b | ||
|
|
7747994c80 | ||
|
|
9b885085c9 | ||
|
|
e605399633 | ||
|
|
a7d52db453 | ||
|
|
c1bbca9ed7 | ||
|
|
be8bf44784 | ||
|
|
15b3cd5f2d | ||
|
|
f926f596aa | ||
|
|
74bccf4caf | ||
|
|
5161c506f0 | ||
|
|
f19ae2f422 | ||
|
|
68a87f55bf | ||
|
|
6d10933a83 | ||
|
|
fa2689454b | ||
|
|
e0222c5f7c | ||
|
|
b60f9f8e08 | ||
|
|
f5b63b52b4 | ||
|
|
e03813c19e | ||
|
|
86ae06e0c8 | ||
|
|
a38eba8449 | ||
|
|
f9bd65a1b5 | ||
|
|
7f9ee81150 | ||
|
|
641e7d5f2e | ||
|
|
f5c40ae571 | ||
|
|
4f6eaf1aac | ||
|
|
c83d486269 | ||
|
|
99db3dc086 | ||
|
|
038efd9f9e | ||
|
|
8c85290c74 | ||
|
|
0fa941e6ef | ||
|
|
26c173c479 | ||
|
|
8f9cf96f3d | ||
|
|
22f3c881a1 | ||
|
|
0d79a01d74 | ||
|
|
a3c373108d | ||
|
|
ea0564126e | ||
|
|
2afaf73fc5 | ||
|
|
e8f676b10b | ||
|
|
9bb6f7eac0 | ||
|
|
07bc34ae7a | ||
|
|
3aa041d2ad | ||
|
|
f7640e35d1 | ||
|
|
efce9a8579 | ||
|
|
5a2f32704c | ||
|
|
177d453491 | ||
|
|
bec6ca9eec | ||
|
|
254a9773ec | ||
|
|
b9dd6432a1 | ||
|
|
d89c0f3e36 | ||
|
|
fd89f35246 | ||
|
|
ee410605e8 | ||
|
|
f884d72a8b | ||
|
|
bd3beb5983 | ||
|
|
44e08b2d66 | ||
|
|
d560e14423 | ||
|
|
7dfbba9b00 | ||
|
|
7ad8db7bdc | ||
|
|
64a9281e6e | ||
|
|
16c71be7f9 | ||
|
|
7d380da5d1 | ||
|
|
a09f4bbd7a | ||
|
|
5b275d6811 | ||
|
|
ea506b904d | ||
|
|
6985d0f476 | ||
|
|
357211e917 | ||
|
|
000cf1ca22 | ||
|
|
a2553f4c3f | ||
|
|
4b9d81cd13 | ||
|
|
e5824ddd35 | ||
|
|
23b6623020 | ||
|
|
bc7311c159 | ||
|
|
39f7e60e8b | ||
|
|
b88044757f | ||
|
|
7f2bd494b5 | ||
|
|
b2dcdc02c7 | ||
|
|
9c7d222a9e | ||
|
|
82cacb1b51 | ||
|
|
f44c3c18a2 | ||
|
|
bfea606bee | ||
|
|
5dfb36ca48 | ||
|
|
654f73f4ee | ||
|
|
5145e355ce | ||
|
|
20cd6d98fc | ||
|
|
12ca34f40f | ||
|
|
fc99fb32a4 | ||
|
|
779887e582 | ||
|
|
6fa2fa16f7 | ||
|
|
bdb1d68b6b | ||
|
|
404bd1450b | ||
|
|
06d28ca663 | ||
|
|
ed1365281f | ||
|
|
824fc48e77 | ||
|
|
8695c412d7 | ||
|
|
e4dfe1125a | ||
|
|
589ece3860 | ||
|
|
a5b7abea0d | ||
|
|
0a58a1fdc3 | ||
|
|
aa2409178b | ||
|
|
960492f1d0 | ||
|
|
726ba873fc | ||
|
|
f1f3e49bc5 | ||
|
|
d00e6de8a2 | ||
|
|
f24f721845 | ||
|
|
b7f5acd332 | ||
|
|
65598e2cd5 | ||
|
|
806421f19f | ||
|
|
9df4026ed4 | ||
|
|
17abda66ba | ||
|
|
f16e9c75b4 | ||
|
|
f9c22ff617 | ||
|
|
2cfc307359 | ||
|
|
66a17bc27f | ||
|
|
5da3974795 | ||
|
|
a8f0d9da9b | ||
|
|
4a3dbc20d6 | ||
|
|
624c4086f1 | ||
|
|
d54b7aa3bf | ||
|
|
1646c7cb83 | ||
|
|
ec0c0df78c | ||
|
|
1d1be0a8f9 | ||
|
|
7afd11fdc6 | ||
|
|
8fcc5622e1 | ||
|
|
8759ed091f | ||
|
|
d2d90ab9da | ||
|
|
2e987647dc | ||
|
|
e96a5114e5 | ||
|
|
6c7223b991 | ||
|
|
b5bcf229ae | ||
|
|
b60536dded | ||
|
|
881091595c | ||
|
|
8f1a91df1b | ||
|
|
2a7926539f | ||
|
|
2017584da4 | ||
|
|
db57de28d6 | ||
|
|
1fcdb18477 | ||
|
|
35561336c6 | ||
|
|
2f9f3e6c72 | ||
|
|
3c02938bfd | ||
|
|
23386c88ea | ||
|
|
0965299e6f | ||
|
|
65485ecdf5 | ||
|
|
3d70271306 | ||
|
|
83d39666d2 | ||
|
|
7409c0ef4e | ||
|
|
b8030ed0a9 | ||
|
|
a537e70459 | ||
|
|
9f38601102 | ||
|
|
f82743af98 | ||
|
|
76f8fc6d9a | ||
|
|
9d5248e2e8 | ||
|
|
9217044b1d | ||
|
|
5d87650553 | ||
|
|
f8d3d4971f | ||
|
|
7e01972cd4 | ||
|
|
f294f1045a | ||
|
|
3086492cbc | ||
|
|
583226392c | ||
|
|
531f8b5a0d | ||
|
|
9f04b3a9f1 | ||
|
|
95a3bbf6b9 | ||
|
|
b458e75098 | ||
|
|
5c526dba82 | ||
|
|
4e84b9ea27 | ||
|
|
cea53ca158 | ||
|
|
40855063c9 | ||
|
|
41a68a1897 | ||
|
|
9c8c1b4311 | ||
|
|
5e383c2148 | ||
|
|
dd88e03b4c | ||
|
|
9a3cb2b5ec | ||
|
|
01084039df | ||
|
|
feb35d2b6e | ||
|
|
3e941cfb0d | ||
|
|
931a34cf7d | ||
|
|
43dc332329 | ||
|
|
c8ae28003f | ||
|
|
cf32f25457 | ||
|
|
fee5309b50 | ||
|
|
b8c7f51d94 | ||
|
|
d00ce2bc11 | ||
|
|
2402655e56 | ||
|
|
283f7a998a | ||
|
|
2b355dbf8c | ||
|
|
ead80e50f9 | ||
|
|
71c53d81c2 | ||
|
|
8498f3df94 | ||
|
|
faa1555294 | ||
|
|
110f70e34c | ||
|
|
cd3b4c46b4 | ||
|
|
60a0657721 | ||
|
|
62c655c927 | ||
|
|
84b0f134b0 | ||
|
|
09902a3f71 | ||
|
|
3db004c733 | ||
|
|
4a566d8823 | ||
|
|
d37e033d35 | ||
|
|
afca84a852 | ||
|
|
9e0c5dab1a | ||
|
|
77eaa2fb5a | ||
|
|
b81ba38d1c | ||
|
|
d5a9050464 | ||
|
|
3c99fe22d3 | ||
|
|
f5b47d0508 | ||
|
|
db84a38519 | ||
|
|
c466e7698a | ||
|
|
6a8283a163 | ||
|
|
1ee334ca68 | ||
|
|
a3a34eb9ef | ||
|
|
db633c90be | ||
|
|
5e0e4dbd4e | ||
|
|
090967c21d | ||
|
|
6c8e9b886d | ||
|
|
2bb2eea226 | ||
|
|
a2bb26a86e | ||
|
|
948a8eb90b | ||
|
|
e66e223189 | ||
|
|
2aee6ac57e | ||
|
|
66abac80d8 | ||
|
|
a1e0d5f834 | ||
|
|
6a1181b21f | ||
|
|
c25ae10873 | ||
|
|
2842c8f669 | ||
|
|
3161892585 | ||
|
|
489843f987 | ||
|
|
dc0b30ab67 | ||
|
|
c3235cc554 | ||
|
|
6568e5918a | ||
|
|
91fba6db99 | ||
|
|
6b7e9a66f1 | ||
|
|
3682b65a4a | ||
|
|
117412645b | ||
|
|
c784ff6925 | ||
|
|
cf477024fc | ||
|
|
d595b7037f | ||
|
|
0c5007c8d8 | ||
|
|
8a87445d1f | ||
|
|
76a693fae4 | ||
|
|
9c150d5f4a | ||
|
|
e5febcdc6c | ||
|
|
1e969a0888 | ||
|
|
72bb549ea3 | ||
|
|
3c7cfdd91f | ||
|
|
b21f80cdd7 | ||
|
|
867a9c77e6 | ||
|
|
bc2d2c18fc | ||
|
|
ab923d245d | ||
|
|
fcf1b9ae03 | ||
|
|
59896e4f89 | ||
|
|
2a9f88c810 | ||
|
|
e98b0a3758 | ||
|
|
0cb6da7be4 | ||
|
|
fc3ff41fc4 | ||
|
|
719ddc8263 | ||
|
|
9f6b1284bb | ||
|
|
bfb4a90fdd | ||
|
|
d1b6bedc30 | ||
|
|
b78c75d829 | ||
|
|
4518f07b5b | ||
|
|
b4e18c50d3 | ||
|
|
d18482a04a | ||
|
|
f3d9dd777d | ||
|
|
e117c4b9a3 | ||
|
|
95666178e5 | ||
|
|
56403466b9 | ||
|
|
c7344ef548 | ||
|
|
71c4abfce8 | ||
|
|
0613af2240 | ||
|
|
dd832ad6df | ||
|
|
8cf7bc9998 | ||
|
|
c1cf11c04c | ||
|
|
0058c40f46 | ||
|
|
1397389f95 | ||
|
|
6a2d3e1d22 | ||
|
|
46ac704013 | ||
|
|
0fdab2a5c5 | ||
|
|
8f4586bfef | ||
|
|
52d0c9861f | ||
|
|
feace61eb4 | ||
|
|
1f5cc8f283 | ||
|
|
60b8520237 | ||
|
|
6a67c5e9a2 | ||
|
|
bcc241518c | ||
|
|
6dfa8f1556 | ||
|
|
19dde692b2 | ||
|
|
14dc93b5d2 | ||
|
|
547620235e | ||
|
|
b43fd0a54b | ||
|
|
8b782c9416 | ||
|
|
aab4e62e24 | ||
|
|
1713fccfc4 | ||
|
|
83ece72ae1 | ||
|
|
d60bcc49e1 | ||
|
|
bc9c37adda | ||
|
|
2583c7f617 | ||
|
|
fea5229e02 | ||
|
|
68be615057 | ||
|
|
370cafcba0 | ||
|
|
f923c1602e | ||
|
|
50a85be872 | ||
|
|
aae4725a3c | ||
|
|
9d76ee9f19 | ||
|
|
34a101b796 | ||
|
|
49b1fd751c | ||
|
|
4c5bf7bb7d | ||
|
|
2d71631d93 | ||
|
|
fa0d933956 | ||
|
|
b5d6384a07 | ||
|
|
d39644a4c9 | ||
|
|
a2feb34dc1 | ||
|
|
7e5fe64153 | ||
|
|
44175d071c | ||
|
|
bae26de444 | ||
|
|
b78707808d | ||
|
|
d41518581a | ||
|
|
4abbfe6142 | ||
|
|
dae813d80c | ||
|
|
af89b178ad | ||
|
|
8c269207fd | ||
|
|
42ecd38517 | ||
|
|
9f7d4dee49 | ||
|
|
458b8e491e | ||
|
|
495e621e69 | ||
|
|
c986512b5f | ||
|
|
d277754ae5 | ||
|
|
2ef2e2f26b | ||
|
|
23a53034fa | ||
|
|
ce57d72a78 | ||
|
|
502b89d890 | ||
|
|
5f0015fad0 | ||
|
|
c81236957b | ||
|
|
970ab38b27 | ||
|
|
8a5c31b81d | ||
|
|
8508fe79b5 | ||
|
|
3859e98801 | ||
|
|
a759c7be9e | ||
|
|
12fc6cf6e2 | ||
|
|
580db6530e | ||
|
|
9c67c237ee | ||
|
|
357d85a72e | ||
|
|
88ad828ce0 | ||
|
|
a95625a34a | ||
|
|
95e00d81f5 | ||
|
|
c2e386a5c5 | ||
|
|
a76aade4ff | ||
|
|
65c9986103 | ||
|
|
9e2b9b6639 | ||
|
|
cf373634d7 | ||
|
|
b3d5d976b4 | ||
|
|
c3c31995ce | ||
|
|
7e92e17429 | ||
|
|
88ab8fa8d7 | ||
|
|
ebe78932bf | ||
|
|
2e613e6d15 | ||
|
|
35ee92db12 | ||
|
|
04d9f760ad | ||
|
|
4f52743be8 | ||
|
|
32cae7a5b2 | ||
|
|
c2c0e3b790 | ||
|
|
6d36a30787 | ||
|
|
48a86ec6de | ||
|
|
5cff914ff3 | ||
|
|
70ea725ce3 | ||
|
|
78f12e45f9 | ||
|
|
e5061acc20 | ||
|
|
2d7bc51d30 | ||
|
|
9128b67ee8 | ||
|
|
551c004476 | ||
|
|
ed6a8d1379 | ||
|
|
766fb89e0b | ||
|
|
c5b8cb4459 | ||
|
|
0deae92829 | ||
|
|
cc5d2f1813 | ||
|
|
41151d0d49 | ||
|
|
52702264a3 | ||
|
|
6e297e1278 | ||
|
|
e3bb9b425f | ||
|
|
79255be79c | ||
|
|
7c836f5ba1 | ||
|
|
938bcd14ad | ||
|
|
229d79fc05 | ||
|
|
2d3dac2e1d | ||
|
|
e23f5e7894 | ||
|
|
571d27c814 | ||
|
|
dde6bd4fe3 | ||
|
|
6e6dbd9329 | ||
|
|
258268f5ef | ||
|
|
9ae49977fb | ||
|
|
d61c54fa60 | ||
|
|
980699af6f | ||
|
|
cc5c280882 | ||
|
|
090456bba1 | ||
|
|
5354d4eb76 | ||
|
|
b986fae611 | ||
|
|
cfcfc3e928 | ||
|
|
f97548fc3a | ||
|
|
36913b425c | ||
|
|
822ea08d89 | ||
|
|
98dd6f3fe7 | ||
|
|
b3f0dbb155 | ||
|
|
6904d931c3 | ||
|
|
529466a9f7 | ||
|
|
77dc695ba1 | ||
|
|
e17776f651 | ||
|
|
0d2f355a74 | ||
|
|
2ce1576016 | ||
|
|
0f3be3c494 | ||
|
|
8c1ca8503a | ||
|
|
32a59c17f4 | ||
|
|
b4b4ceab0b | ||
|
|
be1f27bb9e | ||
|
|
ed10ddb1cb | ||
|
|
dbdb4b0f32 | ||
|
|
59e537362e | ||
|
|
6d96bf414f | ||
|
|
e7ba778a5f | ||
|
|
933d349cd5 | ||
|
|
3de24dcfce | ||
|
|
3275d16b8b | ||
|
|
5bb4cd1dad | ||
|
|
16b14441fa | ||
|
|
93a6272d30 | ||
|
|
0dc526f778 | ||
|
|
183e185812 | ||
|
|
e02453598c | ||
|
|
24af1b5b5f | ||
|
|
5864c283f6 | ||
|
|
be78fa4b91 | ||
|
|
b3abf69a02 | ||
|
|
c530dc11ae | ||
|
|
d368ddbd11 | ||
|
|
e5c6521a15 | ||
|
|
898a59768e | ||
|
|
a85bc93142 | ||
|
|
c6c1f9faa0 | ||
|
|
0eea19c9cc | ||
|
|
ed2270ff46 | ||
|
|
45b6c3b338 | ||
|
|
84e2284f56 | ||
|
|
1c0d0be622 | ||
|
|
a9ce0f487d | ||
|
|
07533e0365 | ||
|
|
ee5ddd4264 | ||
|
|
f519d22d81 | ||
|
|
51ed87086a | ||
|
|
1ca3aa3cdb | ||
|
|
0178c63f6a | ||
|
|
8a97c409fa | ||
|
|
3dd0735305 | ||
|
|
536f775baa | ||
|
|
00f7a684a3 | ||
|
|
d79b166a6a | ||
|
|
b3d827f56a | ||
|
|
40bcef1dcb | ||
|
|
6146f1bdaa | ||
|
|
f5d82d9ef0 | ||
|
|
b2a29ae606 | ||
|
|
98ccba53a2 | ||
|
|
9bfda36647 | ||
|
|
5710cdf19c | ||
|
|
20cf54bfcd | ||
|
|
2ce639e750 | ||
|
|
b1ed413c4f | ||
|
|
b8c3060037 | ||
|
|
c3ea4940d7 | ||
|
|
40e1225b87 | ||
|
|
0c171122b2 | ||
|
|
6d0f4bb3da | ||
|
|
5e2cc6e20c | ||
|
|
99cb43bbea | ||
|
|
ca7d8277f7 | ||
|
|
337d26333e | ||
|
|
ebb64d255b | ||
|
|
7dcb199f68 | ||
|
|
4334e887de | ||
|
|
4e84dc4cc8 | ||
|
|
1a1ed072bf | ||
|
|
ae457f07c4 | ||
|
|
00095942c3 | ||
|
|
d1caa5fc21 | ||
|
|
813e2f97ac | ||
|
|
bcb5a90f5e | ||
|
|
020a1a3149 | ||
|
|
c4d649ec58 | ||
|
|
c02cf2c284 | ||
|
|
c30afd042e | ||
|
|
17640fe6cf | ||
|
|
2e4f6ee420 | ||
|
|
a3768d9221 | ||
|
|
80c3390363 | ||
|
|
a5e3869d8f | ||
|
|
aa7d7c2d02 | ||
|
|
015f205569 | ||
|
|
e59fb15926 | ||
|
|
173c585f2d | ||
|
|
6f8c27793e | ||
|
|
332b81c803 | ||
|
|
4b343b500d | ||
|
|
e87c537642 | ||
|
|
2e6300cce2 | ||
|
|
09514d15a6 | ||
|
|
0de23dcba0 | ||
|
|
bacb153151 | ||
|
|
a01aa299d8 | ||
|
|
44edbddbd8 | ||
|
|
79d677cf3c | ||
|
|
be39b6512f | ||
|
|
fcfeea35da | ||
|
|
7d0eb8c61e | ||
|
|
4d8438a6b6 | ||
|
|
f611244e35 | ||
|
|
546a978d3b | ||
|
|
70b23fb073 | ||
|
|
a56ca597d6 | ||
|
|
679e0228a8 | ||
|
|
e153394323 | ||
|
|
5bd1fcfcfd | ||
|
|
2a392ddc44 | ||
|
|
b5cb8bc0d9 | ||
|
|
fa170bcf98 | ||
|
|
7939d46949 | ||
|
|
ab9df8201a | ||
|
|
4a670ec091 | ||
|
|
10e57e59c4 | ||
|
|
b9ec43ef34 | ||
|
|
42197cd375 | ||
|
|
704852973b | ||
|
|
056b4200df | ||
|
|
250a7d8627 | ||
|
|
1ba51e161e | ||
|
|
32e58af896 | ||
|
|
312fa6fe76 | ||
|
|
afbe0837ba | ||
|
|
36ad2a720f | ||
|
|
901e3b14bb | ||
|
|
588d209f7b | ||
|
|
554c54e6be | ||
|
|
b0fac34ffc | ||
|
|
5ede9f7c6b | ||
|
|
c7254fd23e | ||
|
|
55fcea04af | ||
|
|
c212c0a6b2 | ||
|
|
a31fd6709a | ||
|
|
e367fd2b73 | ||
|
|
1ca67d0241 | ||
|
|
8ffa952ff9 | ||
|
|
da246fa30b | ||
|
|
13f306742e | ||
|
|
f3815dc45e | ||
|
|
d086254012 | ||
|
|
bc4d5ba097 | ||
|
|
c556783fe3 | ||
|
|
5fba4c12aa | ||
|
|
7e0dde3ece | ||
|
|
fc03e83531 | ||
|
|
4c441077c7 | ||
|
|
4a5ca81e9a | ||
|
|
75eebe8f8c | ||
|
|
271a8cdac5 | ||
|
|
25103c1188 | ||
|
|
d81058e606 | ||
|
|
693df54b3b | ||
|
|
ae6ed99dc4 | ||
|
|
14bd58e741 | ||
|
|
6d35a7a4ba | ||
|
|
46b0d1ceac | ||
|
|
67a66d2fcd | ||
|
|
43e90b57ea | ||
|
|
c80740e590 | ||
|
|
54ccb9611e | ||
|
|
8fcb897800 | ||
|
|
699eda5d1b | ||
|
|
d7d0a83550 | ||
|
|
e3c331c911 | ||
|
|
eda4dd6aec | ||
|
|
8ad7be474d | ||
|
|
a64435f155 | ||
|
|
fa160124d2 | ||
|
|
5765cb8330 | ||
|
|
f472b227bb | ||
|
|
d2b419c42e | ||
|
|
09d4de660f | ||
|
|
728dcd8523 | ||
|
|
93cf9bf6a9 | ||
|
|
50841f5e24 | ||
|
|
fc6d92d1fc | ||
|
|
7162a029bb | ||
|
|
d797ddd668 | ||
|
|
989e8c3aa6 | ||
|
|
08b79af242 | ||
|
|
0d2f346a30 | ||
|
|
39f1d5f5fd | ||
|
|
05008bb7f8 | ||
|
|
be90d6fc45 | ||
|
|
a1bcdf9924 | ||
|
|
b0e001393c | ||
|
|
2d08941f6a | ||
|
|
d0fef1f312 | ||
|
|
68342cb0d4 | ||
|
|
2b419212a7 | ||
|
|
b2cbc7e34d | ||
|
|
61247e575b | ||
|
|
31e18266d1 | ||
|
|
df8a8de889 | ||
|
|
8a037d6b29 | ||
|
|
47b555b98c | ||
|
|
0c2dae475e | ||
|
|
dc676d04d8 | ||
|
|
15b54bff50 | ||
|
|
47bd4b4c0b | ||
|
|
3c8b36ddfe | ||
|
|
608df3fddd | ||
|
|
c092c285ee | ||
|
|
93b745e379 | ||
|
|
c18db77ade | ||
|
|
2c0b167e6b | ||
|
|
313254d0c8 | ||
|
|
6f519c97d3 | ||
|
|
17a3e16b1d | ||
|
|
8199358088 | ||
|
|
412928eeaa | ||
|
|
51e1b935bd | ||
|
|
742b51e5e2 | ||
|
|
fdb5e2eebb | ||
|
|
0192f64cd2 | ||
|
|
193298ac87 | ||
|
|
a81cb81799 | ||
|
|
ad8a7fdc9b | ||
|
|
5440afcebe | ||
|
|
715d7e664c | ||
|
|
aa182cfa68 | ||
|
|
f92dd7a872 | ||
|
|
b02b9197d0 | ||
|
|
86d02be70c | ||
|
|
cb990978ee | ||
|
|
a103202c92 | ||
|
|
9d7b133037 | ||
|
|
f727f2a1a9 | ||
|
|
03034768d9 | ||
|
|
aed3e20e08 | ||
|
|
74bac6d986 | ||
|
|
7ebecc353a | ||
|
|
f0302b0d1e | ||
|
|
0b004ad089 | ||
|
|
c9001f068b | ||
|
|
96e0554aae | ||
|
|
31b4aadaba | ||
|
|
f46fa5392a | ||
|
|
3b6a17f193 | ||
|
|
aea77d3b8c | ||
|
|
7cfbe077db | ||
|
|
7bb620f941 | ||
|
|
5b0341a733 | ||
|
|
d99225da1f | ||
|
|
f279180a37 | ||
|
|
9a22018477 | ||
|
|
197119e56d | ||
|
|
dd055ddc5d | ||
|
|
94c3277245 | ||
|
|
fdaf402472 | ||
|
|
bce7764f75 | ||
|
|
70a258aae2 | ||
|
|
77cf00e8e4 | ||
|
|
de05579d1f | ||
|
|
c7d4b722d0 | ||
|
|
02b837c54b | ||
|
|
50e0e88cc2 | ||
|
|
c34245ff21 | ||
|
|
f1a8334f59 | ||
|
|
c8fc4ea500 | ||
|
|
39805bc103 | ||
|
|
2c615682df | ||
|
|
1257e4efac | ||
|
|
5e4a21087e | ||
|
|
2aaef99a54 | ||
|
|
161d3a795d | ||
|
|
9b671cb1a9 | ||
|
|
07d9a9f2c3 | ||
|
|
efabe7f536 | ||
|
|
82aead976e | ||
|
|
17be52c7b6 | ||
|
|
d0a196ec40 | ||
|
|
d484de185d | ||
|
|
96e4e7a4e8 | ||
|
|
4adb34b959 | ||
|
|
819bc12a68 | ||
|
|
eb23e5365f | ||
|
|
84e2faf8a8 | ||
|
|
84f58efc17 | ||
|
|
42254ee4a1 | ||
|
|
7c564aed7a | ||
|
|
8cdcb29274 | ||
|
|
403a369df9 | ||
|
|
5527912cd1 | ||
|
|
c8531dfe37 | ||
|
|
ed8bb2e5a1 | ||
|
|
dd66355488 | ||
|
|
fc3f83231c | ||
|
|
e70c712020 | ||
|
|
1b34aeaec4 | ||
|
|
2566bfa2ed | ||
|
|
ba06f2bbc6 | ||
|
|
1e4fe1680f | ||
|
|
2effb199a1 | ||
|
|
23c139320a | ||
|
|
f65eba606e | ||
|
|
7a2825da9a | ||
|
|
2975eddfe9 | ||
|
|
7f28eae954 | ||
|
|
789be5e942 | ||
|
|
85b114cdfd | ||
|
|
53d063e994 | ||
|
|
0ab081ccbc | ||
|
|
00ce9d64dc | ||
|
|
be20c024aa | ||
|
|
992fb9839a | ||
|
|
96ae2ee7ac | ||
|
|
379cecb08f | ||
|
|
9089b271b3 | ||
|
|
874da8c8d6 | ||
|
|
989580d196 | ||
|
|
03ef54c37b | ||
|
|
ab56dda275 | ||
|
|
3a91f958e3 | ||
|
|
79913a0c9c | ||
|
|
d0fe64ecfa | ||
|
|
4da69685a1 | ||
|
|
bf560dd10d | ||
|
|
7ce76ee28d | ||
|
|
540e9bad29 | ||
|
|
22c2e2c4e5 | ||
|
|
f67d9dcdfa | ||
|
|
edfcadcbdc | ||
|
|
156bcc7d54 | ||
|
|
63ff912d76 | ||
|
|
6cbfaac4f4 | ||
|
|
5574172d99 | ||
|
|
bc8081ebae | ||
|
|
6ed6132c54 | ||
|
|
76c02c98d8 | ||
|
|
012a7885ff | ||
|
|
2b3d41d982 | ||
|
|
a56a48145b | ||
|
|
8dc097e23c | ||
|
|
0323520389 | ||
|
|
e1e395023d | ||
|
|
850214b103 | ||
|
|
02e9805482 | ||
|
|
0feae8402e | ||
|
|
042da53b54 | ||
|
|
aa1b2bace7 | ||
|
|
6b1b4d6015 | ||
|
|
ebeac417e5 | ||
|
|
be005616ea | ||
|
|
ec3a9b0615 | ||
|
|
0b3e651c4b | ||
|
|
426bdd3aa1 | ||
|
|
75e29d61f8 | ||
|
|
dfab283154 | ||
|
|
986c0d7edc | ||
|
|
918c44bc89 | ||
|
|
b8f680d74a | ||
|
|
76fcf6d545 | ||
|
|
c51d25c58b | ||
|
|
1efdba096c | ||
|
|
c4505b7c42 | ||
|
|
d5235bd40b | ||
|
|
353d105c04 | ||
|
|
070cb6c873 | ||
|
|
a066dda0f9 | ||
|
|
ac929c2603 | ||
|
|
9102402a18 | ||
|
|
54a0fc21d8 | ||
|
|
e2b8b7369e | ||
|
|
bcfbe515a4 | ||
|
|
46834ab5ce | ||
|
|
646000920f | ||
|
|
5ea83ccea1 | ||
|
|
03c6473685 | ||
|
|
d37890fac4 | ||
|
|
d5057ea8ea | ||
|
|
2cbebbe9b7 |
39
.editorconfig
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# EditorConfig is awesome: https://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
# Unix-style newlines with a newline ending every file
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
# Matches multiple files with brace expansion notation
|
||||||
|
# Set default charset
|
||||||
|
[*.{js,py}]
|
||||||
|
charset = utf-8# 4 space indentation
|
||||||
|
|
||||||
|
# Swift files
|
||||||
|
[*.swift]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
charset = utf-8# 4 space indentation
|
||||||
|
|
||||||
|
# 4 space indentation
|
||||||
|
[*.py]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
# Tab indentation (no size specified)
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
# Indentation override for all JS under lib directory
|
||||||
|
[lib/**.js]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# Matches the exact files either package.json or .travis.yml
|
||||||
|
[{package.json,.travis.yml}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
* @JoeMatt @lonkelle @nythepegasus @Spidy123222 @SternXD
|
||||||
40
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: Report a bug
|
||||||
|
title: "[BUG] "
|
||||||
|
labels: ["bug"]
|
||||||
|
assignees: []
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Please note that the issue tracker is not for support
|
||||||
|
Thanks for taking the time to fill out this bug report! Before you continue filling out the report, please **[search in GitHub Issues](https://github.com/SideStore/SideStore/issues?q=is%3Aissue+is%3Aopen) for the bug you are experiencing** in case it has already been reported.
|
||||||
|
|
||||||
|
**Please use [Discord](https://discord.gg/sidestore-949183273383395328) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Describe the bug
|
||||||
|
description: What is the bug and how did you discover it?
|
||||||
|
placeholder: Please be clear and concise with your description.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: how-to-reproduce
|
||||||
|
attributes:
|
||||||
|
label: Instructions to reproduce
|
||||||
|
description: Please include clear and consistent instructions for reproducing the bug to make it easier for us to fix it.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: app-version
|
||||||
|
attributes:
|
||||||
|
label: What version of SideStore are you using?
|
||||||
|
description: To retrieve this, go to `Settings` in the SideStore app and scroll down to the bottom.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: other-info
|
||||||
|
attributes:
|
||||||
|
label: Other info
|
||||||
|
description: If you have any other comments, other info that might be useful, or if you found a workaround, please put it here.
|
||||||
10
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# force issue template usage
|
||||||
|
blank_issues_enabled: false
|
||||||
|
|
||||||
|
contact_links:
|
||||||
|
- name: Discord
|
||||||
|
url: https://discord.gg/sidestore-949183273383395328
|
||||||
|
about: If you need support, please go here first instead of making an issue!
|
||||||
|
- name: GitHub Discussions
|
||||||
|
url: https://github.com/SideStore/SideStore/discussions
|
||||||
|
about: As an alternative to Discord, you can also make a new GitHub discussion.
|
||||||
32
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: Suggest a feature
|
||||||
|
title: "[FEATURE REQUEST] "
|
||||||
|
labels: ["enhancement"]
|
||||||
|
assignees: []
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out this feature request! Before you continue filling out the form, please **[search in GitHub Issues](https://github.com/SideStore/SideStore/issues?q=is%3Aissue+is%3Aopen) for the feature you are suggestion** in case it has already been suggested.
|
||||||
|
|
||||||
|
**Please use [Discord](https://discord.gg/sidestore-949183273383395328) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Describe the feature
|
||||||
|
description: What is the feature? How would it work?
|
||||||
|
placeholder: Please be clear and concise with your description.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: use-cases
|
||||||
|
attributes:
|
||||||
|
label: Use cases
|
||||||
|
description: Please include multiple use cases where this feature would be useful.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: Alternatives
|
||||||
|
description: If you have alternative ideas of how this feature could work, you can put them here.
|
||||||
63
.github/maintenance/cache.py
vendored
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import requests
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Your GitHub Personal Access Token
|
||||||
|
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
|
||||||
|
|
||||||
|
# Repository details
|
||||||
|
REPO_OWNER = "SideStore"
|
||||||
|
REPO_NAME = "SideStore"
|
||||||
|
|
||||||
|
|
||||||
|
API_URL = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/actions/caches"
|
||||||
|
|
||||||
|
# Common headers for GitHub API calls
|
||||||
|
HEADERS = {
|
||||||
|
"Accept": "application/vnd.github+json",
|
||||||
|
"Authorization": f"Bearer {GITHUB_TOKEN}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def list_caches():
|
||||||
|
response = requests.get(API_URL, headers=HEADERS)
|
||||||
|
if response.status_code != 200:
|
||||||
|
print(f"Failed to list caches. HTTP {response.status_code}")
|
||||||
|
print("Response:", response.text)
|
||||||
|
sys.exit(1)
|
||||||
|
data = response.json()
|
||||||
|
return data.get("actions_caches", [])
|
||||||
|
|
||||||
|
def delete_cache(cache_id):
|
||||||
|
delete_url = f"{API_URL}/{cache_id}"
|
||||||
|
response = requests.delete(delete_url, headers=HEADERS)
|
||||||
|
return response.status_code
|
||||||
|
|
||||||
|
def main():
|
||||||
|
caches = list_caches()
|
||||||
|
if not caches:
|
||||||
|
print("No caches found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("Found caches:")
|
||||||
|
for cache in caches:
|
||||||
|
print(f"ID: {cache.get('id')}, Key: {cache.get('key')}")
|
||||||
|
|
||||||
|
print("\nDeleting caches...")
|
||||||
|
for cache in caches:
|
||||||
|
cache_id = cache.get("id")
|
||||||
|
status = delete_cache(cache_id)
|
||||||
|
if status == 204:
|
||||||
|
print(f"Successfully deleted cache with ID: {cache_id}")
|
||||||
|
else:
|
||||||
|
print(f"Failed to delete cache with ID: {cache_id}. HTTP status code: {status}")
|
||||||
|
|
||||||
|
print("All caches processed.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
|
|
||||||
|
### How to use
|
||||||
|
'''
|
||||||
|
just export the GITHUB_TOKEN and then run this script via `python3 cache.py' to delete the caches
|
||||||
|
'''
|
||||||
12
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
### Changes
|
||||||
|
|
||||||
|
<!-- Fill this list with what your PR changes. Example: -->
|
||||||
|
- Fix bug
|
||||||
|
- Change UI for QOL
|
||||||
|
|
||||||
|
<!-- If your PR is ready to be merged, you can remove this section. -->
|
||||||
|
### Todo before merge
|
||||||
|
|
||||||
|
<!-- Example: -->
|
||||||
|
- [x] Finish UI changes
|
||||||
|
- [ ] Test
|
||||||
47
.github/workflows/alpha.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
name: Alpha SideStore Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [develop-alpha]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: macos-15
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- run: brew install ldid xcbeautify
|
||||||
|
|
||||||
|
- name: Shared
|
||||||
|
id: shared
|
||||||
|
run: python3 scripts/ci/workflow.py shared
|
||||||
|
|
||||||
|
- name: Beta bump
|
||||||
|
env:
|
||||||
|
RELEASE_CHANNEL: alpha
|
||||||
|
run: python3 scripts/ci/workflow.py bump-beta
|
||||||
|
|
||||||
|
- name: Version
|
||||||
|
id: version
|
||||||
|
run: python3 scripts/ci/workflow.py version
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: python3 scripts/ci/workflow.py build
|
||||||
|
|
||||||
|
- name: Encrypt logs
|
||||||
|
env:
|
||||||
|
BUILD_LOG_ZIP_PASSWORD: ${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
|
||||||
|
run: python3 scripts/ci/workflow.py encrypt-build
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
path: SideStore.ipa
|
||||||
77
.github/workflows/attach_build_products.yml
vendored
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
name: Add artifact links to pull request and related issues
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: [Pull Request SideStore build]
|
||||||
|
types: [completed]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
artifacts-url-comments:
|
||||||
|
name: add artifact links to pull request and related issues job
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
|
steps:
|
||||||
|
- name: add artifact links to pull request and related issues step
|
||||||
|
uses: tonyhallett/artifacts-url-comments@v1.1.0
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
prefix: Builds for this Pull Request are available at
|
||||||
|
suffix: Have a nice day.
|
||||||
|
format: name
|
||||||
|
addTo: pull
|
||||||
|
# addTo: pullandissues
|
||||||
|
nightly-link-comment:
|
||||||
|
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/github-script@v6
|
||||||
|
with:
|
||||||
|
# This snippet is public-domain, taken from
|
||||||
|
# https://github.com/oprypin/nightly.link/blob/master/.github/workflows/pr-comment.yml
|
||||||
|
script: |
|
||||||
|
async function upsertComment(owner, repo, issue_number, purpose, body) {
|
||||||
|
const {data: comments} = await github.rest.issues.listComments(
|
||||||
|
{owner, repo, issue_number});
|
||||||
|
|
||||||
|
const marker = `<!-- bot: ${purpose} -->`;
|
||||||
|
body = marker + "\n" + body;
|
||||||
|
|
||||||
|
const existing = comments.filter((c) => c.body.includes(marker));
|
||||||
|
if (existing.length > 0) {
|
||||||
|
const last = existing[existing.length - 1];
|
||||||
|
core.info(`Updating comment ${last.id}`);
|
||||||
|
await github.rest.issues.updateComment({
|
||||||
|
owner, repo,
|
||||||
|
body,
|
||||||
|
comment_id: last.id,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
core.info(`Creating a comment in issue / PR #${issue_number}`);
|
||||||
|
await github.rest.issues.createComment({issue_number, body, owner, repo});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {owner, repo} = context.repo;
|
||||||
|
const run_id = ${{github.event.workflow_run.id}};
|
||||||
|
|
||||||
|
const pull_requests = ${{ toJSON(github.event.workflow_run.pull_requests) }};
|
||||||
|
if (!pull_requests.length) {
|
||||||
|
return core.error("This workflow doesn't match any pull requests!");
|
||||||
|
}
|
||||||
|
|
||||||
|
const artifacts = await github.paginate(
|
||||||
|
github.rest.actions.listWorkflowRunArtifacts, {owner, repo, run_id});
|
||||||
|
if (!artifacts.length) {
|
||||||
|
return core.error(`No artifacts found`);
|
||||||
|
}
|
||||||
|
let body = `Download the artifacts for this pull request (nightly.link):\n`;
|
||||||
|
for (const art of artifacts) {
|
||||||
|
body += `\n* [${art.name}.zip](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
core.info("Review thread message body:", body);
|
||||||
|
|
||||||
|
for (const pr of pull_requests) {
|
||||||
|
await upsertComment(owner, repo, pr.number,
|
||||||
|
"nightly-link", body);
|
||||||
|
}
|
||||||
168
.github/workflows/nightly.yml
vendored
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
name: Nightly SideStore Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [develop]
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: macos-26
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- run: brew install ldid xcbeautify
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# runtime env setup
|
||||||
|
# --------------------------------------------------
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: 'SideStore/beta-build-num'
|
||||||
|
ref: ${{ env.ref }}
|
||||||
|
token: ${{ secrets.CROSS_REPO_PUSH_KEY }}
|
||||||
|
path: 'Dependencies/beta-build-num'
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Setup
|
||||||
|
run: |
|
||||||
|
BUILD_NUM=$(python3 scripts/ci/workflow.py reserve_build_number 'Dependencies/beta-build-num')
|
||||||
|
MARKETING_VERSION=$(python3 scripts/ci/workflow.py get-marketing-version)
|
||||||
|
SHORT_COMMIT=$(python3 scripts/ci/workflow.py commid-id)
|
||||||
|
|
||||||
|
QUALIFIED_VERSION=$(python3 scripts/ci/workflow.py compute-qualified \
|
||||||
|
"$MARKETING_VERSION" \
|
||||||
|
"$BUILD_NUM" \
|
||||||
|
"${{ env.ref }}" \
|
||||||
|
"$SHORT_COMMIT")
|
||||||
|
|
||||||
|
echo "BUILD_NUM=$BUILD_NUM" >> $GITHUB_ENV
|
||||||
|
echo "MARKETING_VERSION=$MARKETING_VERSION" >> $GITHUB_ENV
|
||||||
|
echo "SHORT_COMMIT=$SHORT_COMMIT" >> $GITHUB_ENV
|
||||||
|
echo "VERSION=$QUALIFIED_VERSION" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Setup Xcode
|
||||||
|
uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||||
|
with:
|
||||||
|
xcode-version: '26.2'
|
||||||
|
|
||||||
|
- name: Restore Cache
|
||||||
|
id: xcode-cache
|
||||||
|
uses: actions/cache/restore@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/Library/Developer/Xcode/DerivedData
|
||||||
|
~/Library/Caches/org.swift.swiftpm
|
||||||
|
key: xcode-build-cache-${{ github.ref_name }}-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
xcode-build-cache-${{ github.ref_name }}-
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# build and test
|
||||||
|
# --------------------------------------------------
|
||||||
|
- name: Clean
|
||||||
|
if: contains(github.event.head_commit.message, '[--clean-build]')
|
||||||
|
run: |
|
||||||
|
python3 scripts/ci/workflow.py clean
|
||||||
|
python3 scripts/ci/workflow.py clean-derived-data
|
||||||
|
python3 scripts/ci/workflow.py clean-spm-cache
|
||||||
|
|
||||||
|
- name: Boot simulator (async)
|
||||||
|
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
|
||||||
|
run: |
|
||||||
|
mkdir -p build/logs
|
||||||
|
python3 scripts/ci/workflow.py boot-sim-async "iPhone 17 Pro"
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
id: build
|
||||||
|
env:
|
||||||
|
BUILD_LOG_ZIP_PASSWORD: ${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
python3 scripts/ci/workflow.py build; STATUS=$?
|
||||||
|
python3 scripts/ci/workflow.py encrypt-build
|
||||||
|
echo "encrypted=true" >> $GITHUB_OUTPUT
|
||||||
|
exit $STATUS
|
||||||
|
|
||||||
|
- name: Tests Build
|
||||||
|
id: test-build
|
||||||
|
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }}
|
||||||
|
env:
|
||||||
|
BUILD_LOG_ZIP_PASSWORD: ${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
python3 scripts/ci/workflow.py tests-build; STATUS=$?
|
||||||
|
python3 scripts/ci/workflow.py encrypt-tests-build
|
||||||
|
exit $STATUS
|
||||||
|
|
||||||
|
- name: Save Cache
|
||||||
|
if: ${{ steps.xcode-cache.outputs.cache-hit != 'true' }}
|
||||||
|
uses: actions/cache/save@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/Library/Developer/Xcode/DerivedData
|
||||||
|
~/Library/Caches/org.swift.swiftpm
|
||||||
|
key: xcode-build-cache-${{ github.ref_name }}-${{ github.sha }}
|
||||||
|
|
||||||
|
- name: Tests Run
|
||||||
|
id: test-run
|
||||||
|
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
|
||||||
|
env:
|
||||||
|
BUILD_LOG_ZIP_PASSWORD: ${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
python3 scripts/ci/workflow.py tests-run "iPhone 17 Pro"; STATUS=$?
|
||||||
|
python3 scripts/ci/workflow.py encrypt-tests-run
|
||||||
|
exit $STATUS
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# artifacts
|
||||||
|
# --------------------------------------------------
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: encrypted-build-logs-${{ env.VERSION }}.zip
|
||||||
|
path: encrypted-build-logs.zip
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }}
|
||||||
|
with:
|
||||||
|
name: encrypted-tests-build-logs-${{ env.SHORT_COMMIT }}.zip
|
||||||
|
path: encrypted-tests-build-logs.zip
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
|
||||||
|
with:
|
||||||
|
name: encrypted-tests-run-logs-${{ env.SHORT_COMMIT }}.zip
|
||||||
|
path: encrypted-tests-run-logs.zip
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: SideStore-${{ env.VERSION }}.ipa
|
||||||
|
path: SideStore.ipa
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: SideStore-${{ env.VERSION }}-dSYMs.zip
|
||||||
|
path: SideStore.dSYMs.zip
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# deploy
|
||||||
|
# --------------------------------------------------
|
||||||
|
- name: Deploy
|
||||||
|
run: |
|
||||||
|
python3 scripts/ci/workflow.py deploy \
|
||||||
|
Dependencies/apps-v2.json \
|
||||||
|
"_includes/source.json" \
|
||||||
|
"${{ env.ref_name }}" \
|
||||||
|
"$SHORT_COMMIT" \
|
||||||
|
"$MARKETING_VERSION" \
|
||||||
|
"$VERSION" \
|
||||||
|
"${{ env.ref_name }}" \
|
||||||
|
"com.SideStore.SideStore" \
|
||||||
|
"SideStore.ipa"
|
||||||
28
.github/workflows/obsolete/alpha.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
name: Alpha SideStore build
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop-alpha
|
||||||
|
|
||||||
|
# cancel duplicate run if from same branch
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Reusable-build:
|
||||||
|
uses: ./.github/workflows/reusable-sidestore-build.yml
|
||||||
|
with:
|
||||||
|
# bundle_id: "com.SideStore.SideStore.Alpha"
|
||||||
|
bundle_id: "com.SideStore.SideStore"
|
||||||
|
# bundle_id_suffix: ".Alpha"
|
||||||
|
is_beta: true
|
||||||
|
publish: ${{ vars.PUBLISH_ALPHA_UPDATES == 'true' }}
|
||||||
|
is_shared_build_num: false
|
||||||
|
release_tag: "alpha"
|
||||||
|
release_name: "Alpha"
|
||||||
|
upstream_tag: "nightly"
|
||||||
|
upstream_name: "Nightly"
|
||||||
|
secrets:
|
||||||
|
CROSS_REPO_PUSH_KEY: ${{ secrets.CROSS_REPO_PUSH_KEY }}
|
||||||
|
BUILD_LOG_ZIP_PASSWORD: ${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
|
||||||
103
.github/workflows/obsolete/beta.yml
vendored
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
name: Beta SideStore build
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+' # example: 1.0.0-beta.1
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build and upload SideStore Beta
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: 'macos-14'
|
||||||
|
version: '15.4'
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: brew install ldid
|
||||||
|
|
||||||
|
- name: Change version to tag
|
||||||
|
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
|
||||||
|
|
||||||
|
- name: Get version
|
||||||
|
id: version
|
||||||
|
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Echo version
|
||||||
|
run: echo "${{ steps.version.outputs.version }}"
|
||||||
|
|
||||||
|
- name: Setup Xcode
|
||||||
|
uses: maxim-lobanov/setup-xcode@v1
|
||||||
|
with:
|
||||||
|
xcode-version: ${{ matrix.version }}
|
||||||
|
|
||||||
|
|
||||||
|
- name: Cache Build
|
||||||
|
uses: irgaly/xcode-cache@v1
|
||||||
|
with:
|
||||||
|
key: xcode-cache-deriveddata-${{ github.sha }}
|
||||||
|
restore-keys: xcode-cache-deriveddata
|
||||||
|
|
||||||
|
- name: Build SideStore
|
||||||
|
run: make build | xcpretty && exit ${PIPESTATUS[0]}
|
||||||
|
|
||||||
|
- name: Fakesign app
|
||||||
|
run: make fakesign
|
||||||
|
|
||||||
|
- name: Convert to IPA
|
||||||
|
run: make ipa
|
||||||
|
|
||||||
|
- name: Get current date
|
||||||
|
id: date
|
||||||
|
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Get current date in AltStore date form
|
||||||
|
id: date_altstore
|
||||||
|
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Upload to new beta release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
name: ${{ steps.version.outputs.version }}
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
draft: true
|
||||||
|
prerelease: true
|
||||||
|
files: SideStore.ipa
|
||||||
|
body: |
|
||||||
|
<!-- NOTE: to reset SideSource cache, go to `https://apps.sidestore.io/reset-cache/nightly/<sidesource key>`. This is not included in the GitHub Action since it makes draft releases so they can be edited and have a changelog. -->
|
||||||
|
Beta builds are hand-picked builds from development commits that will allow you to try out new features earlier than normal. However, **they might contain bugs and other issues. Use at your own risk!**
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
- TODO
|
||||||
|
|
||||||
|
## Build Info
|
||||||
|
|
||||||
|
Built at (UTC): `${{ steps.date.outputs.date }}`
|
||||||
|
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
||||||
|
Commit SHA: `${{ github.sha }}`
|
||||||
|
Version: `${{ steps.version.outputs.version }}`
|
||||||
|
|
||||||
|
- name: Add version to IPA file name
|
||||||
|
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
|
- name: Upload SideStore.ipa Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
|
- name: Upload *.dSYM Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||||
|
path: ./*.dSYM/
|
||||||
34
.github/workflows/obsolete/increase-beta-build-num.sh
vendored
Executable file
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Ensure we are in root directory
|
||||||
|
cd "$(dirname "$0")/../.."
|
||||||
|
|
||||||
|
DATE=`date -u +'%Y.%m.%d'`
|
||||||
|
BUILD_NUM=1
|
||||||
|
|
||||||
|
# Use RELEASE_CHANNEL from the environment variable or default to "beta"
|
||||||
|
RELEASE_CHANNEL=${RELEASE_CHANNEL:-"beta"}
|
||||||
|
|
||||||
|
write() {
|
||||||
|
sed -e "/MARKETING_VERSION = .*/s/$/-$RELEASE_CHANNEL.$DATE.$BUILD_NUM+$(git rev-parse --short HEAD)/" -i '' Build.xcconfig
|
||||||
|
echo "$DATE,$BUILD_NUM" > build_number.txt
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ ! -f "build_number.txt" ]; then
|
||||||
|
write
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
LAST_DATE=`cat build_number.txt | perl -n -e '/([^,]*),([^ ]*)$/ && print $1'`
|
||||||
|
LAST_BUILD_NUM=`cat build_number.txt | perl -n -e '/([^,]*),([^ ]*)$/ && print $2'`
|
||||||
|
|
||||||
|
# if [[ "$DATE" != "$LAST_DATE" ]]; then
|
||||||
|
# write
|
||||||
|
# else
|
||||||
|
# BUILD_NUM=`expr $LAST_BUILD_NUM + 1`
|
||||||
|
# write
|
||||||
|
# fi
|
||||||
|
|
||||||
|
# Build number is always incremental
|
||||||
|
BUILD_NUM=`expr $LAST_BUILD_NUM + 1`
|
||||||
|
write
|
||||||
82
.github/workflows/obsolete/nightly.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
name: Nightly SideStore Build
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *' # Runs every night at midnight UTC
|
||||||
|
workflow_dispatch: # Allows manual trigger
|
||||||
|
|
||||||
|
# cancel duplicate run if from same branch
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-changes:
|
||||||
|
if: github.event_name == 'schedule'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
has_changes: ${{ steps.check.outputs.has_changes }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # Ensure full history
|
||||||
|
|
||||||
|
- name: Get last successful workflow run
|
||||||
|
id: get_last_success
|
||||||
|
run: |
|
||||||
|
LAST_SUCCESS=$(gh run list --workflow "Nightly SideStore Build" --json createdAt,conclusion \
|
||||||
|
--jq '[.[] | select(.conclusion=="success")][0].createdAt' || echo "")
|
||||||
|
echo "Last successful run: $LAST_SUCCESS"
|
||||||
|
echo "last_success=$LAST_SUCCESS" >> $GITHUB_ENV
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Check for new commits since last successful build
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
if [ -n "$LAST_SUCCESS" ]; then
|
||||||
|
NEW_COMMITS=$(git rev-list --count --since="$LAST_SUCCESS" origin/develop)
|
||||||
|
COMMIT_LOG=$(git log --since="$LAST_SUCCESS" --pretty=format:"%h %s" origin/develop)
|
||||||
|
else
|
||||||
|
NEW_COMMITS=1
|
||||||
|
COMMIT_LOG=$(git log -n 10 --pretty=format:"%h %s" origin/develop) # Show last 10 commits if no history
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Has changes: $NEW_COMMITS"
|
||||||
|
echo "New commits since last successful build:"
|
||||||
|
echo "$COMMIT_LOG"
|
||||||
|
|
||||||
|
if [ "$NEW_COMMITS" -gt 0 ]; then
|
||||||
|
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
LAST_SUCCESS: ${{ env.last_success }}
|
||||||
|
|
||||||
|
Reusable-build:
|
||||||
|
if: |
|
||||||
|
always() &&
|
||||||
|
(github.event_name == 'push' ||
|
||||||
|
(github.event_name == 'schedule' && needs.check-changes.result == 'success' && needs.check-changes.outputs.has_changes == 'true'))
|
||||||
|
needs: check-changes
|
||||||
|
uses: ./.github/workflows/reusable-sidestore-build.yml
|
||||||
|
with:
|
||||||
|
# bundle_id: "com.SideStore.SideStore.Nightly"
|
||||||
|
bundle_id: "com.SideStore.SideStore"
|
||||||
|
# bundle_id_suffix: ".Nightly"
|
||||||
|
is_beta: true
|
||||||
|
publish: ${{ vars.PUBLISH_NIGHTLY_UPDATES == 'true' }}
|
||||||
|
is_shared_build_num: false
|
||||||
|
release_tag: "nightly"
|
||||||
|
release_name: "Nightly"
|
||||||
|
upstream_tag: "0.5.10"
|
||||||
|
upstream_name: "Stable"
|
||||||
|
secrets:
|
||||||
|
CROSS_REPO_PUSH_KEY: ${{ secrets.CROSS_REPO_PUSH_KEY }}
|
||||||
|
BUILD_LOG_ZIP_PASSWORD: ${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
|
||||||
|
|
||||||
105
.github/workflows/obsolete/reusable-sidestore-build.yml
vendored
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
name: Reusable SideStore Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
is_beta:
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
publish:
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
is_shared_build_num:
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
type: boolean
|
||||||
|
release_name:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
release_tag:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
upstream_tag:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
upstream_name:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
bundle_id:
|
||||||
|
default: com.SideStore.SideStore
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
bundle_id_suffix:
|
||||||
|
default: ''
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
|
||||||
|
secrets:
|
||||||
|
# GITHUB_TOKEN:
|
||||||
|
# required: true
|
||||||
|
CROSS_REPO_PUSH_KEY:
|
||||||
|
required: true
|
||||||
|
BUILD_LOG_ZIP_PASSWORD:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
|
||||||
|
# since build cache, test-build cache, test-run cache are involved, out of order exec if serialization is on individual jobs will wreak all sorts of havoc
|
||||||
|
# so we serialize on the entire workflow
|
||||||
|
concurrency:
|
||||||
|
group: serialize-workflow
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
shared:
|
||||||
|
uses: ./.github/workflows/sidestore-shared.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs: shared
|
||||||
|
uses: ./.github/workflows/sidestore-build.yml
|
||||||
|
with:
|
||||||
|
is_beta: ${{ inputs.is_beta }}
|
||||||
|
is_shared_build_num: ${{ inputs.is_shared_build_num }}
|
||||||
|
release_tag: ${{ inputs.release_tag }}
|
||||||
|
short_commit: ${{ needs.shared.outputs.short-commit }}
|
||||||
|
bundle_id: ${{ inputs.bundle_id }}
|
||||||
|
bundle_id_suffix: ${{ inputs.bundle_id_suffix }}
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
# tests-build:
|
||||||
|
# if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }}
|
||||||
|
# needs: shared
|
||||||
|
# uses: ./.github/workflows/sidestore-tests-build.yml
|
||||||
|
# with:
|
||||||
|
# release_tag: ${{ inputs.release_tag }}
|
||||||
|
# short_commit: ${{ needs.shared.outputs.short-commit }}
|
||||||
|
# secrets: inherit
|
||||||
|
|
||||||
|
# tests-run:
|
||||||
|
# if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
|
||||||
|
# needs: [shared, tests-build]
|
||||||
|
# uses: ./.github/workflows/sidestore-tests-run.yml
|
||||||
|
# with:
|
||||||
|
# release_tag: ${{ inputs.release_tag }}
|
||||||
|
# short_commit: ${{ needs.shared.outputs.short-commit }}
|
||||||
|
# secrets: inherit
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
# needs: [shared, build, tests-build, tests-run] # Keep tests-run in needs
|
||||||
|
needs: [shared, build] # Keep tests-run in needs
|
||||||
|
if: ${{ always() && (needs.tests-run.result == 'skipped' || needs.tests-run.result == 'success') }}
|
||||||
|
uses: ./.github/workflows/sidestore-deploy.yml
|
||||||
|
with:
|
||||||
|
is_beta: ${{ inputs.is_beta }}
|
||||||
|
publish: ${{ inputs.publish }}
|
||||||
|
release_name: ${{ inputs.release_name }}
|
||||||
|
release_tag: ${{ inputs.release_tag }}
|
||||||
|
upstream_tag: ${{ inputs.upstream_tag }}
|
||||||
|
upstream_name: ${{ inputs.upstream_name }}
|
||||||
|
version: ${{ needs.build.outputs.version }}
|
||||||
|
short_commit: ${{ needs.shared.outputs.short-commit }}
|
||||||
|
release_channel: ${{ needs.build.outputs.release-channel }}
|
||||||
|
marketing_version: ${{ needs.build.outputs.marketing-version }}
|
||||||
|
bundle_id: ${{ inputs.bundle_id }}
|
||||||
|
secrets: inherit
|
||||||
358
.github/workflows/obsolete/sidestore-build.yml
vendored
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
name: SideStore Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
is_beta:
|
||||||
|
type: boolean
|
||||||
|
is_shared_build_num:
|
||||||
|
type: boolean
|
||||||
|
release_tag:
|
||||||
|
type: string
|
||||||
|
bundle_id:
|
||||||
|
type: string
|
||||||
|
bundle_id_suffix:
|
||||||
|
type: string
|
||||||
|
short_commit:
|
||||||
|
type: string
|
||||||
|
secrets:
|
||||||
|
CROSS_REPO_PUSH_KEY:
|
||||||
|
required: true
|
||||||
|
BUILD_LOG_ZIP_PASSWORD:
|
||||||
|
required: false
|
||||||
|
outputs:
|
||||||
|
version:
|
||||||
|
value: ${{ jobs.build.outputs.version }}
|
||||||
|
marketing-version:
|
||||||
|
value: ${{ jobs.build.outputs.marketing-version }}
|
||||||
|
release-channel:
|
||||||
|
value: ${{ jobs.build.outputs.release-channel }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build SideStore - ${{ inputs.release_tag }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: 'macos-26'
|
||||||
|
version: '26.0'
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.version.outputs.version }}
|
||||||
|
marketing-version: ${{ steps.marketing-version.outputs.MARKETING_VERSION }}
|
||||||
|
release-channel: ${{ steps.release-channel.outputs.RELEASE_CHANNEL }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Set beta status
|
||||||
|
run: echo "IS_BETA=${{ inputs.is_beta }}" >> $GITHUB_ENV
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install dependencies - ldid & xcbeautify
|
||||||
|
run: |
|
||||||
|
brew install ldid xcbeautify
|
||||||
|
|
||||||
|
- name: Set ref based on is_shared_build_num
|
||||||
|
if: ${{ inputs.is_beta }}
|
||||||
|
id: set_ref
|
||||||
|
run: |
|
||||||
|
if [ "${{ inputs.is_shared_build_num }}" == "true" ]; then
|
||||||
|
echo "ref=main" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "ref=${{ inputs.release_tag }}" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Checkout SideStore/beta-build-num repo
|
||||||
|
if: ${{ inputs.is_beta }}
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: 'SideStore/beta-build-num'
|
||||||
|
ref: ${{ env.ref }}
|
||||||
|
token: ${{ secrets.CROSS_REPO_PUSH_KEY }}
|
||||||
|
path: 'SideStore/beta-build-num'
|
||||||
|
|
||||||
|
- name: Copy build_number.txt to repo root
|
||||||
|
if: ${{ inputs.is_beta }}
|
||||||
|
run: |
|
||||||
|
cp SideStore/beta-build-num/build_number.txt .
|
||||||
|
echo "cat build_number.txt"
|
||||||
|
cat build_number.txt
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Echo Build.xcconfig
|
||||||
|
run: |
|
||||||
|
echo "cat Build.xcconfig"
|
||||||
|
cat Build.xcconfig
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Set Release Channel info for build number bumper
|
||||||
|
id: release-channel
|
||||||
|
run: |
|
||||||
|
RELEASE_CHANNEL="${{ inputs.release_tag }}"
|
||||||
|
echo "RELEASE_CHANNEL=${RELEASE_CHANNEL}" >> $GITHUB_ENV
|
||||||
|
echo "RELEASE_CHANNEL=${RELEASE_CHANNEL}" >> $GITHUB_OUTPUT
|
||||||
|
echo "RELEASE_CHANNEL=${RELEASE_CHANNEL}"
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Increase build number for beta builds
|
||||||
|
if: ${{ inputs.is_beta }}
|
||||||
|
run: |
|
||||||
|
bash .github/workflows/increase-beta-build-num.sh
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Extract MARKETING_VERSION from Build.xcconfig
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
version=$(grep MARKETING_VERSION Build.xcconfig | sed -e 's/MARKETING_VERSION = //g')
|
||||||
|
echo "version=$version" >> $GITHUB_OUTPUT
|
||||||
|
echo "version=$version"
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Set MARKETING_VERSION
|
||||||
|
if: ${{ inputs.is_beta }}
|
||||||
|
id: marketing-version
|
||||||
|
run: |
|
||||||
|
# Extract version number (e.g., "0.6.0")
|
||||||
|
version=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/^[^0-9]*([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
|
||||||
|
# Extract date (YYYYMMDD) (e.g., "20250205")
|
||||||
|
date=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/.*\.([0-9]{4})\.([0-9]{2})\.([0-9]{2})\..*/\1\2\3/')
|
||||||
|
# Extract build number (e.g., "2")
|
||||||
|
build_num=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/.*\.([0-9]+)\+.*/\1/')
|
||||||
|
|
||||||
|
# Combine them into the final output
|
||||||
|
MARKETING_VERSION="${version}-${date}.${build_num}+${{ inputs.short_commit }}"
|
||||||
|
|
||||||
|
echo "MARKETING_VERSION=$MARKETING_VERSION" >> $GITHUB_ENV
|
||||||
|
echo "MARKETING_VERSION=$MARKETING_VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "MARKETING_VERSION=$MARKETING_VERSION"
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Echo Updated Build.xcconfig, build_number.txt
|
||||||
|
if: ${{ inputs.is_beta }}
|
||||||
|
run: |
|
||||||
|
cat Build.xcconfig
|
||||||
|
cat build_number.txt
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Setup Xcode
|
||||||
|
uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||||
|
with:
|
||||||
|
xcode-version: ${{ matrix.version }}
|
||||||
|
|
||||||
|
- name: (Build) Restore Xcode & SwiftPM Cache (Exact match)
|
||||||
|
id: xcode-cache-restore
|
||||||
|
uses: actions/cache/restore@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/Library/Developer/Xcode/DerivedData
|
||||||
|
~/Library/Caches/org.swift.swiftpm
|
||||||
|
key: xcode-cache-build-${{ github.ref_name }}-${{ github.sha }}
|
||||||
|
|
||||||
|
- name: (Build) Restore Xcode & SwiftPM Cache (Last Available)
|
||||||
|
id: xcode-cache-restore-recent
|
||||||
|
uses: actions/cache/restore@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/Library/Developer/Xcode/DerivedData
|
||||||
|
~/Library/Caches/org.swift.swiftpm
|
||||||
|
key: xcode-cache-build-${{ github.ref_name }}-
|
||||||
|
|
||||||
|
# - name: (Build) Cache Build
|
||||||
|
# uses: irgaly/xcode-cache@v1.8.1
|
||||||
|
# with:
|
||||||
|
# key: xcode-cache-deriveddata-build-${{ github.ref_name }}-${{ github.sha }}
|
||||||
|
# restore-keys: xcode-cache-deriveddata-build-${{ github.ref_name }}-
|
||||||
|
# swiftpm-cache-key: xcode-cache-sourcedata-build-${{ github.ref_name }}-${{ github.sha }}
|
||||||
|
# swiftpm-cache-restore-keys: |
|
||||||
|
# xcode-cache-sourcedata-build-${{ github.ref_name }}-
|
||||||
|
|
||||||
|
- name: (Build) Clean previous build artifacts
|
||||||
|
# using 'tee' to intercept stdout and log for detailed build-log
|
||||||
|
run: |
|
||||||
|
make clean
|
||||||
|
mkdir -p build/logs
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: (Build) List Files and derived data
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||||
|
ls -la .
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||||
|
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
|
||||||
|
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||||
|
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
- name: Set BundleID Suffix for Sidestore build
|
||||||
|
run: |
|
||||||
|
echo "BUNDLE_ID_SUFFIX=${{ inputs.bundle_id_suffix }}" >> $GITHUB_ENV
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
|
||||||
|
- name: Build SideStore.xcarchive
|
||||||
|
# using 'tee' to intercept stdout and log for detailed build-log
|
||||||
|
run: |
|
||||||
|
NSUnbufferedIO=YES make -B build 2>&1 | tee -a build/logs/build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Fakesign app
|
||||||
|
run: make fakesign | tee -a build/logs/build.log
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Convert to IPA
|
||||||
|
run: make ipa | tee -a build/logs/build.log
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: (Build) Save Xcode & SwiftPM Cache
|
||||||
|
id: cache-save
|
||||||
|
if: ${{ steps.xcode-cache-restore.outputs.cache-hit != 'true' }}
|
||||||
|
uses: actions/cache/save@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/Library/Developer/Xcode/DerivedData
|
||||||
|
~/Library/Caches/org.swift.swiftpm
|
||||||
|
key: xcode-cache-build-${{ github.ref_name }}-${{ github.sha }}
|
||||||
|
|
||||||
|
- name: (Build) List Files and Build artifacts
|
||||||
|
run: |
|
||||||
|
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||||
|
ls -la .
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> Build <<<<<<<<<<"
|
||||||
|
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||||
|
find SideStore -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> SideStore.xcarchive <<<<<<<<<<"
|
||||||
|
find SideStore.xcarchive -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||||
|
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Encrypt build-logs for upload
|
||||||
|
id: encrypt-build-log
|
||||||
|
run: |
|
||||||
|
DEFAULT_BUILD_LOG_PASSWORD=12345
|
||||||
|
|
||||||
|
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
|
||||||
|
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
|
||||||
|
|
||||||
|
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
|
||||||
|
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
|
||||||
|
fi
|
||||||
|
|
||||||
|
pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-build-logs.zip * || popd
|
||||||
|
echo "::set-output name=encrypted::true"
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Upload encrypted-build-logs.zip
|
||||||
|
id: attach-encrypted-build-log
|
||||||
|
if: ${{ always() && steps.encrypt-build-log.outputs.encrypted == 'true' }}
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: encrypted-build-logs-${{ steps.version.outputs.version }}.zip
|
||||||
|
path: encrypted-build-logs.zip
|
||||||
|
|
||||||
|
- name: Upload SideStore.ipa Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
path: SideStore.ipa
|
||||||
|
|
||||||
|
- name: Zip dSYMs
|
||||||
|
run: zip -r -9 ./SideStore.dSYMs.zip ./SideStore.xcarchive/dSYMs
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Upload *.dSYM Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: SideStore-${{ steps.version.outputs.version }}-dSYMs.zip
|
||||||
|
path: SideStore.dSYMs.zip
|
||||||
|
|
||||||
|
- name: Keep rolling the build numbers for each successful build
|
||||||
|
if: ${{ inputs.is_beta }}
|
||||||
|
run: |
|
||||||
|
pushd SideStore/beta-build-num/
|
||||||
|
|
||||||
|
echo "Configure Git user (committer details)"
|
||||||
|
git config user.name "GitHub Actions"
|
||||||
|
git config user.email "github-actions@github.com"
|
||||||
|
|
||||||
|
echo "Adding files to commit"
|
||||||
|
git add --verbose build_number.txt
|
||||||
|
git commit -m " - updated for ${{ inputs.release_tag }} - ${{ inputs.short_commit }} deployment" || echo "No changes to commit"
|
||||||
|
|
||||||
|
echo "Pushing to remote repo"
|
||||||
|
git push --verbose
|
||||||
|
popd
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Get last successful commit
|
||||||
|
id: get_last_commit
|
||||||
|
run: |
|
||||||
|
# Try to get the last successful workflow run commit
|
||||||
|
LAST_SUCCESS_SHA=$(gh run list --branch "${{ github.ref_name }}" --status success --json headSha --jq '.[0].headSha')
|
||||||
|
echo "LAST_SUCCESS_SHA=$LAST_SUCCESS_SHA" >> $GITHUB_OUTPUT
|
||||||
|
echo "LAST_SUCCESS_SHA=$LAST_SUCCESS_SHA" >> $GITHUB_ENV
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Create release notes
|
||||||
|
run: |
|
||||||
|
LAST_SUCCESS_SHA=${{ steps.get_last_commit.outputs.LAST_SUCCESS_SHA}}
|
||||||
|
echo "Last successful commit SHA: $LAST_SUCCESS_SHA"
|
||||||
|
|
||||||
|
FROM_COMMIT=$LAST_SUCCESS_SHA
|
||||||
|
# Check if we got a valid SHA
|
||||||
|
if [ -z "$LAST_SUCCESS_SHA" ] || [ "$LAST_SUCCESS_SHA" = "null" ]; then
|
||||||
|
echo "No successful run found, using initial commit of branch"
|
||||||
|
# Get the first commit of the branch (initial commit)
|
||||||
|
FROM_COMMIT=$(git rev-list --max-parents=0 HEAD)
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 update_release_notes.py $FROM_COMMIT ${{ inputs.release_tag }} ${{ github.ref_name }}
|
||||||
|
# cat release-notes.md
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Upload release-notes.md
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: release-notes-${{ inputs.short_commit }}.md
|
||||||
|
path: release-notes.md
|
||||||
|
|
||||||
|
- name: Upload update_release_notes.py
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: update_release_notes-${{ inputs.short_commit }}.py
|
||||||
|
path: update_release_notes.py
|
||||||
|
|
||||||
|
- name: Upload update_apps.py
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: update_apps-${{ inputs.short_commit }}.py
|
||||||
|
path: update_apps.py
|
||||||
281
.github/workflows/obsolete/sidestore-deploy.yml
vendored
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
name: SideStore Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
is_beta:
|
||||||
|
type: boolean
|
||||||
|
publish:
|
||||||
|
type: boolean
|
||||||
|
release_name:
|
||||||
|
type: string
|
||||||
|
release_tag:
|
||||||
|
type: string
|
||||||
|
upstream_tag:
|
||||||
|
type: string
|
||||||
|
upstream_name:
|
||||||
|
type: string
|
||||||
|
version:
|
||||||
|
type: string
|
||||||
|
short_commit:
|
||||||
|
type: string
|
||||||
|
marketing_version:
|
||||||
|
type: string
|
||||||
|
release_channel:
|
||||||
|
type: string
|
||||||
|
bundle_id:
|
||||||
|
type: string
|
||||||
|
secrets:
|
||||||
|
CROSS_REPO_PUSH_KEY:
|
||||||
|
required: true
|
||||||
|
# GITHUB_TOKEN:
|
||||||
|
# required: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Deploy SideStore - ${{ inputs.release_tag }}
|
||||||
|
runs-on: macos-15
|
||||||
|
steps:
|
||||||
|
- name: Download IPA artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: SideStore-${{ inputs.version }}.ipa
|
||||||
|
|
||||||
|
- name: Download dSYM artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: SideStore-${{ inputs.version }}-dSYMs.zip
|
||||||
|
|
||||||
|
- name: Download encrypted-build-logs artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: encrypted-build-logs-${{ inputs.version }}.zip
|
||||||
|
|
||||||
|
- name: Download encrypted-tests-build-logs artifact
|
||||||
|
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }}
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: encrypted-tests-build-logs-${{ inputs.short_commit }}.zip
|
||||||
|
|
||||||
|
- name: Download encrypted-tests-run-logs artifact
|
||||||
|
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: encrypted-tests-run-logs-${{ inputs.short_commit }}.zip
|
||||||
|
|
||||||
|
- name: Download tests-recording artifact
|
||||||
|
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: tests-recording-${{ inputs.short_commit }}.mp4
|
||||||
|
|
||||||
|
- name: Download test-results artifact
|
||||||
|
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: test-results-${{ inputs.short_commit }}.zip
|
||||||
|
|
||||||
|
- name: Download release-notes.md
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: release-notes-${{ inputs.short_commit }}.md
|
||||||
|
|
||||||
|
- name: Download update_release_notes.py
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: update_release_notes-${{ inputs.short_commit }}.py
|
||||||
|
|
||||||
|
- name: Download update_apps.py
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: update_apps-${{ inputs.short_commit }}.py
|
||||||
|
|
||||||
|
- name: Read release notes
|
||||||
|
id: release_notes
|
||||||
|
run: |
|
||||||
|
CONTENT=$(python3 update_release_notes.py --retrieve ${{ inputs.release_tag }})
|
||||||
|
echo "content<<EOF" >> $GITHUB_OUTPUT
|
||||||
|
echo "$CONTENT" >> $GITHUB_OUTPUT
|
||||||
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: List files before upload
|
||||||
|
run: |
|
||||||
|
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||||
|
find . -maxdepth 4 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Get current date
|
||||||
|
id: date
|
||||||
|
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Get current date in AltStore date form
|
||||||
|
id: date_altstore
|
||||||
|
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
|
||||||
|
- name: List files to upload
|
||||||
|
id: list_uploads
|
||||||
|
run: |
|
||||||
|
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||||
|
find . -maxdepth 4 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
FILES="SideStore.ipa SideStore.dSYMs.zip encrypted-build-logs.zip"
|
||||||
|
|
||||||
|
if [[ "${{ vars.ENABLE_TESTS }}" == "1" && "${{ vars.ENABLE_TESTS_BUILD }}" == "1" ]]; then
|
||||||
|
FILES="$FILES encrypted-tests-build-logs.zip"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${{ vars.ENABLE_TESTS }}" == "1" && "${{ vars.ENABLE_TESTS_RUN }}" == "1" ]]; then
|
||||||
|
FILES="$FILES encrypted-tests-run-logs.zip test-results.zip tests-recording.mp4"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Final upload list:"
|
||||||
|
for f in $FILES; do
|
||||||
|
if [[ -f "$f" ]]; then
|
||||||
|
echo " ✓ $f"
|
||||||
|
else
|
||||||
|
echo " - $f (missing)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "files=$FILES" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Set Upstream Recommendation
|
||||||
|
id: upstream_recommendation
|
||||||
|
run: |
|
||||||
|
UPSTREAM_NAME=$(echo "${{ inputs.upstream_name }}" | tr '[:upper:]' '[:lower:]')
|
||||||
|
if [[ "$UPSTREAM_NAME" != "nightly" ]]; then
|
||||||
|
echo "content<<EOF" >> $GITHUB_OUTPUT
|
||||||
|
echo "If you want to try out new features early but want a lower chance of bugs, you can look at [SideStore ${{ inputs.upstream_name }}](https://github.com/${{ github.repository }}/releases?q=${{ inputs.upstream_tag }})." >> $GITHUB_OUTPUT
|
||||||
|
echo "" >> $GITHUB_OUTPUT
|
||||||
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "content=" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Upload to releases
|
||||||
|
uses: IsaacShelton/update-existing-release@v1.3.1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
release: ${{ inputs.release_name }}
|
||||||
|
tag: ${{ inputs.release_tag }}
|
||||||
|
prerelease: ${{ inputs.is_beta }}
|
||||||
|
files: ${{ steps.list_uploads.outputs.files }}
|
||||||
|
body: |
|
||||||
|
This is an ⚠️ **EXPERIMENTAL** ⚠️ ${{ inputs.release_name }} build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}).
|
||||||
|
|
||||||
|
${{ inputs.release_name }} builds are **extremely experimental builds only meant to be used by developers and beta testers. They often contain bugs and experimental features. Use at your own risk!**
|
||||||
|
|
||||||
|
${{ steps.upstream_recommendation.outputs.content }}
|
||||||
|
## Build Info
|
||||||
|
|
||||||
|
Built at (UTC): `${{ steps.date.outputs.date }}`
|
||||||
|
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
||||||
|
Commit SHA: `${{ github.sha }}`
|
||||||
|
Version: `${{ inputs.version }}`
|
||||||
|
|
||||||
|
${{ steps.release_notes.outputs.content }}
|
||||||
|
|
||||||
|
- name: Get formatted date
|
||||||
|
run: |
|
||||||
|
FORMATTED_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
echo "Formatted date: $FORMATTED_DATE"
|
||||||
|
echo "FORMATTED_DATE=$FORMATTED_DATE" >> $GITHUB_ENV
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Get size of IPA in bytes (macOS/Linux)
|
||||||
|
run: |
|
||||||
|
if [[ "$(uname)" == "Darwin" ]]; then
|
||||||
|
# macOS
|
||||||
|
IPA_SIZE=$(stat -f %z SideStore.ipa)
|
||||||
|
else
|
||||||
|
# Linux
|
||||||
|
IPA_SIZE=$(stat -c %s SideStore.ipa)
|
||||||
|
fi
|
||||||
|
echo "IPA size in bytes: $IPA_SIZE"
|
||||||
|
echo "IPA_SIZE=$IPA_SIZE" >> $GITHUB_ENV
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Compute SHA-256 of IPA
|
||||||
|
run: |
|
||||||
|
SHA256_HASH=$(shasum -a 256 SideStore.ipa | awk '{ print $1 }')
|
||||||
|
echo "SHA-256 Hash: $SHA256_HASH"
|
||||||
|
echo "SHA256_HASH=$SHA256_HASH" >> $GITHUB_ENV
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Set Release Info variables
|
||||||
|
run: |
|
||||||
|
echo "IS_BETA=${{ inputs.is_beta }}" >> $GITHUB_ENV
|
||||||
|
echo "BUNDLE_IDENTIFIER=${{ inputs.bundle_id }}" >> $GITHUB_ENV
|
||||||
|
echo "VERSION_IPA=${{ inputs.marketing_version }}" >> $GITHUB_ENV
|
||||||
|
echo "VERSION_DATE=$FORMATTED_DATE" >> $GITHUB_ENV
|
||||||
|
echo "RELEASE_CHANNEL=${{ inputs.release_channel }}" >> $GITHUB_ENV
|
||||||
|
echo "SIZE=$IPA_SIZE" >> $GITHUB_ENV
|
||||||
|
echo "SHA256=$SHA256_HASH" >> $GITHUB_ENV
|
||||||
|
echo "DOWNLOAD_URL=https://github.com/SideStore/SideStore/releases/download/${{ inputs.release_tag }}/SideStore.ipa" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# Format localized description
|
||||||
|
get_description() {
|
||||||
|
cat <<EOF
|
||||||
|
This is release for:
|
||||||
|
- version: "${{ inputs.version }}"
|
||||||
|
- revision: "${{ inputs.short_commit }}"
|
||||||
|
- timestamp: "${{ steps.date.outputs.date }}"
|
||||||
|
|
||||||
|
Release Notes:
|
||||||
|
${{ steps.release_notes.outputs.content }}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
LOCALIZED_DESCRIPTION=$(get_description)
|
||||||
|
echo "$LOCALIZED_DESCRIPTION"
|
||||||
|
|
||||||
|
# multiline strings
|
||||||
|
echo "LOCALIZED_DESCRIPTION<<EOF" >> $GITHUB_ENV
|
||||||
|
echo "$LOCALIZED_DESCRIPTION" >> $GITHUB_ENV
|
||||||
|
echo "EOF" >> $GITHUB_ENV
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Check if Publish updates is set
|
||||||
|
id: check_publish
|
||||||
|
run: |
|
||||||
|
echo "Publish updates to source.json = ${{ inputs.publish }}"
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Checkout SideStore/apps-v2.json
|
||||||
|
if: ${{ inputs.is_beta && inputs.publish }}
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: 'SideStore/apps-v2.json'
|
||||||
|
ref: 'main' # this branch is shared by all beta builds, so beta build workflows are serialized
|
||||||
|
token: ${{ secrets.CROSS_REPO_PUSH_KEY }}
|
||||||
|
path: 'SideStore/apps-v2.json'
|
||||||
|
|
||||||
|
# for stable builds, let the user manually edit the source.json
|
||||||
|
- name: Publish to SideStore/apps-v2.json
|
||||||
|
if: ${{ inputs.is_beta && inputs.publish }}
|
||||||
|
id: publish-release
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
# Copy and execute the update script
|
||||||
|
pushd SideStore/apps-v2.json/
|
||||||
|
|
||||||
|
# Configure Git user (committer details)
|
||||||
|
git config user.name "GitHub Actions"
|
||||||
|
git config user.email "github-actions@github.com"
|
||||||
|
|
||||||
|
# update the source.json
|
||||||
|
python3 ../../update_apps.py "./_includes/source.json"
|
||||||
|
|
||||||
|
# Commit changes and push using SSH
|
||||||
|
git add --verbose ./_includes/source.json
|
||||||
|
git commit -m " - updated for ${{ inputs.short_commit }} deployment" || echo "No changes to commit"
|
||||||
|
|
||||||
|
git push --verbose
|
||||||
|
popd
|
||||||
24
.github/workflows/obsolete/sidestore-shared.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: SideStore Shared
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
outputs:
|
||||||
|
short-commit:
|
||||||
|
value: ${{ jobs.shared.outputs.short-commit }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
shared:
|
||||||
|
name: Shared Steps
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
runs-on: 'macos-15'
|
||||||
|
steps:
|
||||||
|
- name: Set short commit hash
|
||||||
|
id: commit-id
|
||||||
|
run: |
|
||||||
|
# SHORT_COMMIT="${{ github.sha }}"
|
||||||
|
SHORT_COMMIT=${GITHUB_SHA:0:7}
|
||||||
|
echo "Short commit hash: $SHORT_COMMIT"
|
||||||
|
echo "SHORT_COMMIT=$SHORT_COMMIT" >> $GITHUB_OUTPUT
|
||||||
|
outputs:
|
||||||
|
short-commit: ${{ steps.commit-id.outputs.SHORT_COMMIT }}
|
||||||
165
.github/workflows/obsolete/sidestore-tests-build.yml
vendored
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
name: SideStore Tests Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
release_tag:
|
||||||
|
type: string
|
||||||
|
short_commit:
|
||||||
|
type: string
|
||||||
|
secrets:
|
||||||
|
BUILD_LOG_ZIP_PASSWORD:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tests-build:
|
||||||
|
name: Tests-Build SideStore - ${{ inputs.release_tag }}
|
||||||
|
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: 'macos-26'
|
||||||
|
version: '26.0'
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Install dependencies - xcbeautify
|
||||||
|
run: |
|
||||||
|
brew install xcbeautify
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Setup Xcode
|
||||||
|
uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||||
|
with:
|
||||||
|
xcode-version: '26.0'
|
||||||
|
|
||||||
|
# - name: (Tests-Build) Cache Build
|
||||||
|
# uses: irgaly/xcode-cache@v1.8.1
|
||||||
|
# with:
|
||||||
|
# key: xcode-cache-deriveddata-test-${{ github.ref_name }}-${{ github.sha }}
|
||||||
|
# # tests shouldn't restore cache unless it is same build
|
||||||
|
# # restore-keys: xcode-cache-deriveddata-test-${{ github.ref_name }}-
|
||||||
|
# swiftpm-cache-key: xcode-cache-sourcedata-test-${{ github.ref_name }}-${{ github.sha }}
|
||||||
|
# swiftpm-cache-restore-keys: |
|
||||||
|
# xcode-cache-sourcedata-test-${{ github.ref_name }}-
|
||||||
|
# delete-used-deriveddata-cache: true
|
||||||
|
|
||||||
|
- name: (Tests-Build) Restore Xcode & SwiftPM Cache (Exact match)
|
||||||
|
id: xcode-cache-restore
|
||||||
|
uses: actions/cache/restore@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/Library/Developer/Xcode/DerivedData
|
||||||
|
~/Library/Caches/org.swift.swiftpm
|
||||||
|
key: xcode-cache-tests-${{ github.ref_name }}-${{ github.sha }}
|
||||||
|
|
||||||
|
- name: (Tests-Build) Restore Xcode & SwiftPM Cache (Last Available)
|
||||||
|
id: xcode-cache-restore-recent
|
||||||
|
uses: actions/cache/restore@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/Library/Developer/Xcode/DerivedData
|
||||||
|
~/Library/Caches/org.swift.swiftpm
|
||||||
|
key: xcode-cache-tests-${{ github.ref_name }}-
|
||||||
|
|
||||||
|
- name: Clean Derived Data (if required)
|
||||||
|
if: ${{ vars.PERFORM_CLEAN_TESTS_BUILD == '1' }}
|
||||||
|
run: |
|
||||||
|
rm -rf ~/Library/Developer/Xcode/DerivedData/
|
||||||
|
make clean
|
||||||
|
xcodebuild clean
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: (Tests-Build) Clean previous build artifacts
|
||||||
|
run: |
|
||||||
|
make clean
|
||||||
|
mkdir -p build/logs
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: (Tests-Build) List Files and derived data
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||||
|
ls -la .
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||||
|
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
|
||||||
|
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||||
|
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
- name: Build SideStore Tests
|
||||||
|
# using 'tee' to intercept stdout and log for detailed build-log
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
NSUnbufferedIO=YES make -B build-tests 2>&1 | tee -a build/logs/tests-build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
|
||||||
|
|
||||||
|
- name: (Tests-Build) Save Xcode & SwiftPM Cache
|
||||||
|
id: cache-save
|
||||||
|
if: ${{ steps.xcode-cache-restore.outputs.cache-hit != 'true' }}
|
||||||
|
uses: actions/cache/save@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/Library/Developer/Xcode/DerivedData
|
||||||
|
~/Library/Caches/org.swift.swiftpm
|
||||||
|
key: xcode-cache-tests-${{ github.ref_name }}-${{ github.sha }}
|
||||||
|
|
||||||
|
- name: (Tests-Build) List Files and Build artifacts
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||||
|
ls -la .
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> Build <<<<<<<<<<"
|
||||||
|
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||||
|
find ~/Library/Developer/Xcode/DerivedData -maxdepth 8 -exec ls -ld {} + | grep "Build/Products" >> tests-build-deriveddata.txt || true
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: tests-build-deriveddata-${{ inputs.short_commit }}.txt
|
||||||
|
path: tests-build-deriveddata.txt
|
||||||
|
|
||||||
|
- name: Encrypt tests-build-logs for upload
|
||||||
|
id: encrypt-test-log
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
DEFAULT_BUILD_LOG_PASSWORD=12345
|
||||||
|
|
||||||
|
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
|
||||||
|
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
|
||||||
|
|
||||||
|
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
|
||||||
|
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
|
||||||
|
fi
|
||||||
|
|
||||||
|
pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-tests-build-logs.zip * || popd
|
||||||
|
echo "::set-output name=encrypted::true"
|
||||||
|
|
||||||
|
- name: Upload encrypted-tests-build-logs.zip
|
||||||
|
id: attach-encrypted-test-log
|
||||||
|
if: always() && steps.encrypt-test-log.outputs.encrypted == 'true'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: encrypted-tests-build-logs-${{ inputs.short_commit }}.zip
|
||||||
|
path: encrypted-tests-build-logs.zip
|
||||||
196
.github/workflows/obsolete/sidestore-tests-run.yml
vendored
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
name: SideStore Tests Run
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
release_tag:
|
||||||
|
type: string
|
||||||
|
short_commit:
|
||||||
|
type: string
|
||||||
|
secrets:
|
||||||
|
BUILD_LOG_ZIP_PASSWORD:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tests-run:
|
||||||
|
name: Tests-Run SideStore - ${{ inputs.release_tag }}
|
||||||
|
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: 'macos-26'
|
||||||
|
version: '26.0'
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Boot Simulator async(nohup) for testing
|
||||||
|
run: |
|
||||||
|
mkdir -p build/logs
|
||||||
|
nohup make -B boot-sim-async </dev/null >> build/logs/tests-run.log 2>&1 &
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Setup Xcode
|
||||||
|
uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||||
|
with:
|
||||||
|
xcode-version: '26.0'
|
||||||
|
|
||||||
|
# - name: (Tests-Run) Cache Build
|
||||||
|
# uses: irgaly/xcode-cache@v1.8.1
|
||||||
|
# with:
|
||||||
|
# # This comes from
|
||||||
|
# key: xcode-cache-deriveddata-test-${{ github.ref_name }}-${{ github.sha }}
|
||||||
|
# swiftpm-cache-key: xcode-cache-sourcedata-test-${{ github.ref_name }}-${{ github.sha }}
|
||||||
|
|
||||||
|
- name: (Tests-Build) Restore Xcode & SwiftPM Cache (Exact match) [from tests-build job]
|
||||||
|
id: xcode-cache-restore
|
||||||
|
uses: actions/cache/restore@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/Library/Developer/Xcode/DerivedData
|
||||||
|
~/Library/Caches/org.swift.swiftpm
|
||||||
|
key: xcode-cache-tests-${{ github.ref_name }}-${{ github.sha }}
|
||||||
|
|
||||||
|
- name: (Tests-Run) Clean previous build artifacts
|
||||||
|
run: |
|
||||||
|
make clean
|
||||||
|
mkdir -p build/logs
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: (Tests-Run) List Files and derived data
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||||
|
ls -la .
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||||
|
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
|
||||||
|
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||||
|
find ~/Library/Developer/Xcode/DerivedData -maxdepth 8 -exec ls -ld {} + | grep "Build/Products" >> tests-run-deriveddata.txt || true
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: tests-run-deriveddata-${{ inputs.short_commit }}.txt
|
||||||
|
path: tests-run-deriveddata.txt
|
||||||
|
|
||||||
|
# we expect simulator to have been booted by now, so exit otherwise
|
||||||
|
- name: Simulator Boot Check
|
||||||
|
run: |
|
||||||
|
mkdir -p build/logs
|
||||||
|
make -B sim-boot-check | tee -a build/logs/tests-run.log
|
||||||
|
exit ${PIPESTATUS[0]}
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Start Recording UI tests (if DEBUG_RECORD_TESTS is set to 1)
|
||||||
|
if: ${{ vars.DEBUG_RECORD_TESTS == '1' }}
|
||||||
|
run: |
|
||||||
|
nohup xcrun simctl io booted recordVideo -f tests-recording.mp4 --codec h264 </dev/null > tests-recording.log 2>&1 &
|
||||||
|
RECORD_PID=$!
|
||||||
|
echo "RECORD_PID=$RECORD_PID" >> $GITHUB_ENV
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Run SideStore Tests
|
||||||
|
# using 'tee' to intercept stdout and log for detailed build-log
|
||||||
|
run: |
|
||||||
|
make run-tests 2>&1 | tee -a build/logs/tests-run.log && exit ${PIPESTATUS[0]}
|
||||||
|
# NSUnbufferedIO=YES make -B run-tests 2>&1 | tee build/logs/tests-run.log | xcpretty -r junit --output ./build/tests/test-results.xml && exit ${PIPESTATUS[0]}
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Stop Recording tests
|
||||||
|
if: ${{ always() && env.RECORD_PID != '' }}
|
||||||
|
run: |
|
||||||
|
kill -INT ${{ env.RECORD_PID }}
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: (Tests-Run) List Files and Build artifacts
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||||
|
ls -la .
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> Build <<<<<<<<<<"
|
||||||
|
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Encrypt tests-run-logs for upload
|
||||||
|
id: encrypt-test-log
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
DEFAULT_BUILD_LOG_PASSWORD=12345
|
||||||
|
|
||||||
|
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
|
||||||
|
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
|
||||||
|
|
||||||
|
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
|
||||||
|
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
|
||||||
|
fi
|
||||||
|
|
||||||
|
pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-tests-run-logs.zip * || popd
|
||||||
|
echo "::set-output name=encrypted::true"
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Upload encrypted-tests-run-logs.zip
|
||||||
|
id: attach-encrypted-test-log
|
||||||
|
if: always() && steps.encrypt-test-log.outputs.encrypted == 'true'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: encrypted-tests-run-logs-${{ inputs.short_commit }}.zip
|
||||||
|
path: encrypted-tests-run-logs.zip
|
||||||
|
|
||||||
|
- name: Print tests-recording.log contents (if exists)
|
||||||
|
if: ${{ always() && env.RECORD_PID != '' }}
|
||||||
|
run: |
|
||||||
|
if [ -f tests-recording.log ]; then
|
||||||
|
echo "tests-recording.log found. Its contents:"
|
||||||
|
cat tests-recording.log
|
||||||
|
else
|
||||||
|
echo "tests-recording.log not found."
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Check for tests-recording.mp4 presence
|
||||||
|
id: check-recording
|
||||||
|
if: ${{ always() && env.RECORD_PID != '' }}
|
||||||
|
run: |
|
||||||
|
if [ -f tests-recording.mp4 ]; then
|
||||||
|
echo "::set-output name=found::true"
|
||||||
|
echo "tests-recording.mp4 found."
|
||||||
|
else
|
||||||
|
echo "tests-recording.mp4 not found, skipping upload."
|
||||||
|
echo "::set-output name=found::false"
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Upload tests-recording.mp4
|
||||||
|
id: upload-recording
|
||||||
|
if: ${{ always() && steps.check-recording.outputs.found == 'true' }}
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: tests-recording-${{ inputs.short_commit }}.mp4
|
||||||
|
path: tests-recording.mp4
|
||||||
|
|
||||||
|
- name: Zip test-results
|
||||||
|
run: zip -r -9 ./test-results.zip ./build/tests
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Upload Test Artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: test-results-${{ inputs.short_commit }}.zip
|
||||||
|
path: test-results.zip
|
||||||
98
.github/workflows/pr.yml
vendored
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
name: Pull Request SideStore build
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
# types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]
|
||||||
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build and upload SideStore
|
||||||
|
if: ${{ github.event.pull_request.draft == false }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: 'macos-14'
|
||||||
|
version: '16.1'
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: brew install ldid
|
||||||
|
|
||||||
|
- name: Install xcbeautify
|
||||||
|
run: brew install xcbeautify
|
||||||
|
|
||||||
|
- name: Add PR suffix to version
|
||||||
|
run: sed -e "/MARKETING_VERSION = .*/s/\$/-pr.${{ github.event.pull_request.number }}+$(git rev-parse --short ${COMMIT:-HEAD})/" -i '' Build.xcconfig
|
||||||
|
env:
|
||||||
|
COMMIT: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
|
- name: Get version
|
||||||
|
id: version
|
||||||
|
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Echo version
|
||||||
|
run: echo "${{ steps.version.outputs.version }}"
|
||||||
|
|
||||||
|
- name: Setup Xcode
|
||||||
|
uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||||
|
with:
|
||||||
|
xcode-version: ${{ matrix.version }}
|
||||||
|
|
||||||
|
- name: Cache Build
|
||||||
|
uses: irgaly/xcode-cache@v1
|
||||||
|
with:
|
||||||
|
key: xcode-cache-deriveddata-${{ github.sha }}
|
||||||
|
restore-keys: xcode-cache-deriveddata-
|
||||||
|
swiftpm-cache-key: xcode-cache-sourcedata-${{ github.sha }}
|
||||||
|
swiftpm-cache-restore-keys: |
|
||||||
|
xcode-cache-sourcedata-
|
||||||
|
|
||||||
|
- name: List Files and derived data
|
||||||
|
run: |
|
||||||
|
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||||
|
ls -la .
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||||
|
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
|
||||||
|
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||||
|
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
|
||||||
|
- name: Build SideStore
|
||||||
|
run: NSUnbufferedIO=YES make build 2>&1 | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
|
||||||
|
|
||||||
|
- name: Fakesign app
|
||||||
|
run: make fakesign
|
||||||
|
|
||||||
|
- name: Convert to IPA
|
||||||
|
run: make ipa
|
||||||
|
|
||||||
|
- name: Add version to IPA file name
|
||||||
|
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
|
- name: Upload SideStore.ipa Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
|
||||||
|
- name: Upload *.dSYM Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||||
|
path: ./SideStore.xcarchive/dSYMs/*
|
||||||
242
.github/workflows/stable.yml
vendored
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
name: Stable SideStore build
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '[0-9]+.[0-9]+.[0-9]+' # example: 1.0.0
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build SideStore - stable (on tag push)
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: 'macos-26'
|
||||||
|
version: '26.0'
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Echo Build.xcconfig
|
||||||
|
run: |
|
||||||
|
echo "cat Build.xcconfig"
|
||||||
|
cat Build.xcconfig
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
# - name: Change MARKETING_VERSION to the pushed tag that triggered this build
|
||||||
|
# run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
|
||||||
|
|
||||||
|
- name: Echo Updated Build.xcconfig
|
||||||
|
run: |
|
||||||
|
cat Build.xcconfig
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Extract MARKETING_VERSION from Build.xcconfig
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
version=$(grep MARKETING_VERSION Build.xcconfig | sed -e 's/MARKETING_VERSION = //g')
|
||||||
|
echo "version=$version" >> $GITHUB_OUTPUT
|
||||||
|
echo "version=$version"
|
||||||
|
|
||||||
|
echo "MARKETING_VERSION=$version" >> $GITHUB_ENV
|
||||||
|
echo "MARKETING_VERSION=$version" >> $GITHUB_OUTPUT
|
||||||
|
echo "MARKETING_VERSION=$version"
|
||||||
|
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Fail the build if pushed tag and embedded MARKETING_VERSION in Build.xcconfig are mismatching
|
||||||
|
run: |
|
||||||
|
if [ "$MARKETING_VERSION" != "${{ github.ref_name }}" ]; then
|
||||||
|
echo 'Version mismatch: $tag != $marketing_version ... '
|
||||||
|
echo " expected-tag : $MARKETING_VERSION"
|
||||||
|
echo " pushed-tag : ${{ github.ref_name }}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo 'Version matches: $tag == $marketing_version ... '
|
||||||
|
echo " expected-tag : $MARKETING_VERSION"
|
||||||
|
echo " pushed-tag : ${{ github.ref_name }}"
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Install dependencies - ldid & xcbeautify
|
||||||
|
run: |
|
||||||
|
brew install ldid xcbeautify
|
||||||
|
|
||||||
|
- name: Setup Xcode
|
||||||
|
uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||||
|
with:
|
||||||
|
xcode-version: ${{ matrix.version }}
|
||||||
|
|
||||||
|
- name: (Build) Restore Xcode & SwiftPM Cache (Exact match)
|
||||||
|
id: xcode-cache-restore
|
||||||
|
uses: actions/cache/restore@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/Library/Developer/Xcode/DerivedData
|
||||||
|
~/Library/Caches/org.swift.swiftpm
|
||||||
|
key: xcode-cache-build-stable-${{ github.sha }}
|
||||||
|
|
||||||
|
- name: (Build) Restore Xcode & SwiftPM Cache (Last Available)
|
||||||
|
id: xcode-cache-restore-recent
|
||||||
|
uses: actions/cache/restore@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/Library/Developer/Xcode/DerivedData
|
||||||
|
~/Library/Caches/org.swift.swiftpm
|
||||||
|
key: xcode-cache-build-stable-
|
||||||
|
|
||||||
|
- name: (Build) Clean previous build artifacts
|
||||||
|
run: |
|
||||||
|
make clean
|
||||||
|
mkdir -p build/logs
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: (Build) List Files and derived data
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||||
|
ls -la .
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||||
|
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
|
||||||
|
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||||
|
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
- name: Build SideStore.xcarchive
|
||||||
|
# using 'tee' to intercept stdout and log for detailed build-log
|
||||||
|
run: |
|
||||||
|
NSUnbufferedIO=YES make -B build 2>&1 | tee -a build/logs/build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Fakesign app
|
||||||
|
run: make fakesign | tee -a build/logs/build.log
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Convert to IPA
|
||||||
|
run: make ipa | tee -a build/logs/build.log
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: (Build) Save Xcode & SwiftPM Cache
|
||||||
|
id: cache-save
|
||||||
|
if: ${{ steps.xcode-cache-restore.outputs.cache-hit != 'true' }}
|
||||||
|
uses: actions/cache/save@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/Library/Developer/Xcode/DerivedData
|
||||||
|
~/Library/Caches/org.swift.swiftpm
|
||||||
|
key: xcode-cache-build-stable-${{ github.sha }}
|
||||||
|
|
||||||
|
- name: (Build) List Files and Build artifacts
|
||||||
|
run: |
|
||||||
|
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||||
|
ls -la .
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> Build <<<<<<<<<<"
|
||||||
|
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||||
|
find SideStore -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> SideStore.xcarchive <<<<<<<<<<"
|
||||||
|
find SideStore.xcarchive -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||||
|
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Encrypt build-logs for upload
|
||||||
|
id: encrypt-build-log
|
||||||
|
run: |
|
||||||
|
DEFAULT_BUILD_LOG_PASSWORD=12345
|
||||||
|
|
||||||
|
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
|
||||||
|
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
|
||||||
|
|
||||||
|
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
|
||||||
|
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
|
||||||
|
fi
|
||||||
|
|
||||||
|
pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-build-logs.zip * || popd
|
||||||
|
echo "::set-output name=encrypted::true"
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Upload encrypted-build-logs.zip
|
||||||
|
id: attach-encrypted-build-log
|
||||||
|
if: ${{ always() && steps.encrypt-build-log.outputs.encrypted == 'true' }}
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: encrypted-build-logs-${{ steps.version.outputs.version }}.zip
|
||||||
|
path: encrypted-build-logs.zip
|
||||||
|
|
||||||
|
- name: Upload SideStore.ipa Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
path: SideStore.ipa
|
||||||
|
|
||||||
|
- name: Zip dSYMs
|
||||||
|
run: zip -r -9 ./SideStore.dSYMs.zip ./SideStore.xcarchive/dSYMs
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Upload *.dSYM Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: SideStore-${{ steps.version.outputs.version }}-dSYMs.zip
|
||||||
|
path: SideStore.dSYMs.zip
|
||||||
|
|
||||||
|
- name: Get current date
|
||||||
|
id: date
|
||||||
|
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Get current date in AltStore date form
|
||||||
|
id: date_altstore
|
||||||
|
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Upload to releases
|
||||||
|
uses: IsaacShelton/update-existing-release@v1.3.1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
draft: true
|
||||||
|
release: ${{ github.ref_name }} # name
|
||||||
|
tag: ${{ github.ref_name }}
|
||||||
|
# stick with what the user pushed, do not use latest commit or anything,
|
||||||
|
# ex: if we want to go back to previous release due to hot issue, dev can create a new tag pointing to that older working tag/commit so as to keep it as an update (to revert major issue)
|
||||||
|
# in this case we do not want the tag to be auto-updated to latest
|
||||||
|
updateTag: false
|
||||||
|
prerelease: false
|
||||||
|
files: >
|
||||||
|
SideStore.ipa
|
||||||
|
SideStore.dSYMs.zip
|
||||||
|
encrypted-build-logs.zip
|
||||||
|
body: |
|
||||||
|
<!-- NOTE: to reset SideSource cache, go to `https://apps.sidestore.io/reset-cache/nightly/<sidesource key>`. This is not included in the GitHub Action since it makes draft releases so they can be edited and have a changelog. -->
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
- TODO
|
||||||
|
|
||||||
|
## Build Info
|
||||||
|
|
||||||
|
Built at (UTC): `${{ steps.date.outputs.date }}`
|
||||||
|
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
||||||
|
Commit SHA: `${{ github.sha }}`
|
||||||
|
Version: `${{ steps.version.outputs.version }}`
|
||||||
46
.gitignore
vendored
@@ -1,14 +1,18 @@
|
|||||||
# macOS
|
# macOS
|
||||||
#
|
#
|
||||||
*.DS_Store
|
**/*.DS_Store
|
||||||
|
|
||||||
# Xcode
|
# Xcode
|
||||||
#
|
#
|
||||||
|
|
||||||
|
## CocoaPods
|
||||||
|
Pods/
|
||||||
|
|
||||||
## Build generated
|
## Build generated
|
||||||
build/
|
build/
|
||||||
DerivedData
|
DerivedData
|
||||||
|
|
||||||
|
SideStore.xcarchive
|
||||||
## Various settings
|
## Various settings
|
||||||
*.pbxuser
|
*.pbxuser
|
||||||
!default.pbxuser
|
!default.pbxuser
|
||||||
@@ -27,4 +31,42 @@ xcuserdata
|
|||||||
*.xcscmblueprint
|
*.xcscmblueprint
|
||||||
|
|
||||||
## Obj-C/Swift specific
|
## Obj-C/Swift specific
|
||||||
*.hmap
|
*.hmap
|
||||||
|
/newrelic_agent.log
|
||||||
|
/CodeSigning.xcconfig
|
||||||
|
/.vscode
|
||||||
|
|
||||||
|
## AppCode specific
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
Payload/
|
||||||
|
**/SideStore.ipa
|
||||||
|
**/AltBackup.ipa
|
||||||
|
**/*.dSYM
|
||||||
|
|
||||||
|
Dependencies/.*-prebuilt-fetch-*
|
||||||
|
SideStore/minimuxer/*
|
||||||
|
SideStore/em_proxy/*
|
||||||
|
!Dependencies/**/.gitkeep
|
||||||
|
.nightly-build-num
|
||||||
|
|
||||||
|
## em_proxy and minimuxer biaries
|
||||||
|
**/.last-prebuilt-fetch-em_proxy
|
||||||
|
**/.last-prebuilt-fetch-minimuxer
|
||||||
|
|
||||||
|
# misc
|
||||||
|
**/output.txt
|
||||||
|
SideStore/.skip-prebuilt-fetch-minimuxer
|
||||||
|
SideStore/.skip-prebuilt-fetch-em_proxy
|
||||||
|
|
||||||
|
.git.bkp/
|
||||||
|
# Never check-in this package.resolved file
|
||||||
|
# coz SPM then resolves packages using the stale entries in this file
|
||||||
|
*.xcodeproj/**/Package.resolved
|
||||||
|
*.xcworkspace/**/Package.resolved
|
||||||
|
|
||||||
|
# some more commandline build artifacts
|
||||||
|
test-recording.mp4
|
||||||
|
test-recording.log
|
||||||
|
altstore-sources.md
|
||||||
|
local-build.sh
|
||||||
76
.gitmodules
vendored
@@ -1,18 +1,68 @@
|
|||||||
|
#-------------------------------
|
||||||
|
# When changing url/branch in this .gitmodules file,
|
||||||
|
# Always ensure you run:
|
||||||
|
# 1. `git rm --cached <submodule_relative_path>` # this removes the submodule entry from general git tracking
|
||||||
|
# 2. `rm -rf .git/modules/<submodule_relative_path>` # this removes the stale name entries in submodule tracker
|
||||||
|
# 3. `rm -rf <submodule_relative_path>` # removes the submodule completely
|
||||||
|
# 4. `git submodule --deinit <submodule_relative_path>` # make sure that the submodule is de-inited too (ignore errors at this point)
|
||||||
|
# 5. `git submodule add [-b <branch_name>] <repo_url> <submodule_relative_path>` # This adds the submodule back into general git tracking and also adds to the submodule tracker
|
||||||
|
# 6. Step 5 creates an entry in the .gitmodules when a submodule is added,
|
||||||
|
# So if you already had one entry, try to remove duplicates at this point
|
||||||
|
# 7. `git submodule sync --recursive` # this now sets/updates the submodule repo url tracker into git config
|
||||||
|
# 8. `git submodule update --init --recursive` # this now clones the updated repo set by .gitmodules
|
||||||
|
# But this will always fetch the latest commit sepecified by the custom(if set)/default branch
|
||||||
|
# 9. If you do want to have a specific commit in that submodule branch and not latest, you need to perform normal detached head checkout and check-in as follows:
|
||||||
|
# `pushd <submodule_relative_path>` # switch to the submodule repo
|
||||||
|
# `git checkout <commit-id>` # this creates a detached head state
|
||||||
|
# `popd` # get back to parent repo
|
||||||
|
# `git add <submodule_relative_path>` # check-in the changes in parent for this submodule link (tracker)
|
||||||
|
# `git commit -m <commit-message>` # commit it to parent repo
|
||||||
|
# `git push` # push to parent repo to preserve this entire change in the submodule repo/link file
|
||||||
|
#
|
||||||
|
# NOTES:
|
||||||
|
# 1. updating just this .gitmodules file is NOT ENOUGH when changing repo url and performing a simple `git submodule update --init --recursive`, need to do all the above listed steps for proper tracking
|
||||||
|
# 2. updating the branch in this .gitmodules for same repo is okay as long as `git submodule update --init --recursive` is also performed followed by it
|
||||||
|
# 3. Ensure there is no stale entries or duplicate entries in this .gitmodules file coz, `git submodule add ...` creates an entry here.
|
||||||
|
#-------------------------------
|
||||||
|
|
||||||
[submodule "Dependencies/Roxas"]
|
[submodule "Dependencies/Roxas"]
|
||||||
path = Dependencies/Roxas
|
path = Dependencies/Roxas
|
||||||
url = https://github.com/rileytestut/Roxas.git
|
url = https://github.com/rileytestut/Roxas.git
|
||||||
[submodule "Dependencies/AltSign"]
|
|
||||||
path = Dependencies/AltSign
|
|
||||||
url = https://github.com/rileytestut/AltSign.git
|
|
||||||
[submodule "Dependencies/libimobiledevice"]
|
[submodule "Dependencies/libimobiledevice"]
|
||||||
path = Dependencies/libimobiledevice
|
path = Dependencies/libimobiledevice
|
||||||
url = https://github.com/rileytestut/libimobiledevice.git
|
url = https://github.com/SideStore/libimobiledevice
|
||||||
[submodule "Dependencies/libusbmuxd"]
|
[submodule "Dependencies/libusbmuxd"]
|
||||||
path = Dependencies/libusbmuxd
|
path = Dependencies/libusbmuxd
|
||||||
url = https://github.com/libimobiledevice/libusbmuxd.git
|
url = https://github.com/libimobiledevice/libusbmuxd.git
|
||||||
[submodule "Dependencies/libplist"]
|
[submodule "Dependencies/libplist"]
|
||||||
path = Dependencies/libplist
|
path = Dependencies/libplist
|
||||||
url = https://github.com/libimobiledevice/libplist.git
|
url = https://github.com/SideStore/libplist.git
|
||||||
[submodule "Dependencies/MarkdownAttributedString"]
|
[submodule "Dependencies/MarkdownAttributedString"]
|
||||||
path = Dependencies/MarkdownAttributedString
|
path = Dependencies/MarkdownAttributedString
|
||||||
url = https://github.com/chockenberry/MarkdownAttributedString.git
|
url = https://github.com/chockenberry/MarkdownAttributedString.git
|
||||||
|
[submodule "Dependencies/libimobiledevice-glue"]
|
||||||
|
path = Dependencies/libimobiledevice-glue
|
||||||
|
url = https://github.com/libimobiledevice/libimobiledevice-glue
|
||||||
|
|
||||||
|
|
||||||
|
#sidestore dependencies
|
||||||
|
[submodule "Dependencies/minimuxer"]
|
||||||
|
path = Dependencies/minimuxer
|
||||||
|
url = https://github.com/SideStore/minimuxer
|
||||||
|
branch = master
|
||||||
|
[submodule "Dependencies/em_proxy"]
|
||||||
|
path = Dependencies/em_proxy
|
||||||
|
url = https://github.com/SideStore/em_proxy
|
||||||
|
branch = master
|
||||||
|
[submodule "Dependencies/libfragmentzip"]
|
||||||
|
path = Dependencies/libfragmentzip
|
||||||
|
url = https://github.com/SideStore/libfragmentzip
|
||||||
|
branch = master
|
||||||
|
[submodule "Dependencies/apps-v2.json"]
|
||||||
|
path = Dependencies/apps-v2.json
|
||||||
|
url = https://github.com/SideStore/apps-v2.json
|
||||||
|
branch = main
|
||||||
|
[submodule "Dependencies/AltSign"]
|
||||||
|
path = Dependencies/AltSign
|
||||||
|
url = https://github.com/SideStore/AltSign
|
||||||
|
branch = master
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>group.com.rileytestut.AltStore</string>
|
<string>group.$(GROUP_ID)</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import UIKit
|
|||||||
|
|
||||||
extension AppDelegate
|
extension AppDelegate
|
||||||
{
|
{
|
||||||
static let startBackupNotification = Notification.Name("io.altstore.StartBackup")
|
static let startBackupNotification = Notification.Name("io.sidestore.StartBackup")
|
||||||
static let startRestoreNotification = Notification.Name("io.altstore.StartRestore")
|
static let startRestoreNotification = Notification.Name("io.sidestore.StartRestore")
|
||||||
|
|
||||||
static let operationDidFinishNotification = Notification.Name("io.altstore.BackupOperationFinished")
|
static let operationDidFinishNotification = Notification.Name("io.sidestore.BackupOperationFinished")
|
||||||
|
|
||||||
static let operationResultKey = "result"
|
static let operationResultKey = "result"
|
||||||
}
|
}
|
||||||
@@ -88,14 +88,25 @@ private extension AppDelegate
|
|||||||
|
|
||||||
@objc func operationDidFinish(_ notification: Notification)
|
@objc func operationDidFinish(_ notification: Notification)
|
||||||
{
|
{
|
||||||
defer { self.currentBackupReturnURL = nil }
|
defer {
|
||||||
|
self.currentBackupReturnURL = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: @mahee96: This doesn't account cases where backup is too long and user switched to other apps
|
||||||
|
// The check for self.currentBackupReturnURL when backup/restore was still in progress but app switched
|
||||||
|
// between FG/BG is improper, since it will ignore(eat up) the response(success/failure) to parent
|
||||||
|
//
|
||||||
|
// This leaves the backup/restore to show dummy animation forever
|
||||||
guard
|
guard
|
||||||
let returnURL = self.currentBackupReturnURL,
|
let returnURL = self.currentBackupReturnURL,
|
||||||
let result = notification.userInfo?[AppDelegate.operationResultKey] as? Result<Void, Error>
|
let result = notification.userInfo?[AppDelegate.operationResultKey] as? Result<Void, Error>
|
||||||
else { return }
|
else {
|
||||||
|
return // This is bad (Needs fixing - never eat up response like this unless there is no context to post response to!)
|
||||||
|
}
|
||||||
|
|
||||||
guard var components = URLComponents(url: returnURL, resolvingAgainstBaseURL: false) else { return }
|
guard var components = URLComponents(url: returnURL, resolvingAgainstBaseURL: false) else {
|
||||||
|
return // This is ASSERTION Failure, ie RETURN URL needs to be valid. So ignoring (eating up) response is not the solution
|
||||||
|
}
|
||||||
|
|
||||||
switch result
|
switch result
|
||||||
{
|
{
|
||||||
@@ -112,6 +123,7 @@ private extension AppDelegate
|
|||||||
guard let responseURL = components.url else { return }
|
guard let responseURL = components.url else { return }
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
// Response to the caller/parent app is posted here (url is provided by caller in incoming query params)
|
||||||
UIApplication.shared.open(responseURL, options: [:]) { (success) in
|
UIApplication.shared.open(responseURL, options: [:]) { (success) in
|
||||||
print("Sent response to app with success:", success)
|
print("Sent response to app with success:", success)
|
||||||
}
|
}
|
||||||
|
|||||||
57
AltBackup/BackupController.swift
Normal file → Executable file
@@ -26,20 +26,60 @@ extension Error
|
|||||||
|
|
||||||
struct BackupError: ALTLocalizedError
|
struct BackupError: ALTLocalizedError
|
||||||
{
|
{
|
||||||
enum Code
|
enum Code: ALTErrorEnum, RawRepresentable
|
||||||
{
|
{
|
||||||
case invalidBundleID
|
case invalidBundleID
|
||||||
case appGroupNotFound(String?)
|
case appGroupNotFound(String?)
|
||||||
case randomError // Used for debugging.
|
case randomError // Used for debugging.
|
||||||
|
|
||||||
|
// Provide failure reason for each error code
|
||||||
|
var errorFailureReason: String {
|
||||||
|
switch self {
|
||||||
|
case .invalidBundleID:
|
||||||
|
return NSLocalizedString("The bundle identifier is invalid.", comment: "")
|
||||||
|
case .appGroupNotFound(let appGroup):
|
||||||
|
if let appGroup = appGroup {
|
||||||
|
return String(format: NSLocalizedString("The app group “%@” could not be found.", comment: ""), appGroup)
|
||||||
|
} else {
|
||||||
|
return NSLocalizedString("The AltStore app group could not be found.", comment: "")
|
||||||
|
}
|
||||||
|
case .randomError:
|
||||||
|
return NSLocalizedString("A random error occurred.", comment: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static var errorDomain: String {
|
||||||
|
return "com.sidestore.BackupError"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a raw value for RawRepresentable conformance
|
||||||
|
var rawValue: Int {
|
||||||
|
switch self {
|
||||||
|
case .invalidBundleID: return 0
|
||||||
|
case .appGroupNotFound: return 1
|
||||||
|
case .randomError: return 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initializer for RawRepresentable
|
||||||
|
init?(rawValue: Int) {
|
||||||
|
switch rawValue {
|
||||||
|
case 0: self = .invalidBundleID
|
||||||
|
case 1: self = .appGroupNotFound(nil)
|
||||||
|
case 2: self = .randomError
|
||||||
|
default: return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let code: Code
|
let code: Code
|
||||||
|
|
||||||
let sourceFile: String
|
let sourceFile: String
|
||||||
let sourceFileLine: Int
|
let sourceFileLine: Int
|
||||||
|
|
||||||
var failure: String?
|
var failure: String?
|
||||||
|
|
||||||
|
var errorTitle: String?
|
||||||
|
var errorFailure: String?
|
||||||
|
|
||||||
var failureReason: String? {
|
var failureReason: String? {
|
||||||
switch self.code
|
switch self.code
|
||||||
{
|
{
|
||||||
@@ -66,12 +106,19 @@ struct BackupError: ALTLocalizedError
|
|||||||
return userInfo.compactMapValues { $0 }
|
return userInfo.compactMapValues { $0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Implement description for CustomStringConvertible
|
||||||
|
var description: String {
|
||||||
|
return "\(errorTitle ?? "Unknown Error"): \(failureReason ?? "No reason available")"
|
||||||
|
}
|
||||||
|
|
||||||
init(_ code: Code, description: String? = nil, file: String = #file, line: Int = #line)
|
init(_ code: Code, description: String? = nil, file: String = #file, line: Int = #line)
|
||||||
{
|
{
|
||||||
self.code = code
|
self.code = code
|
||||||
self.failure = description
|
self.failure = description
|
||||||
self.sourceFile = file
|
self.sourceFile = file
|
||||||
self.sourceFileLine = line
|
self.sourceFileLine = line
|
||||||
|
self.errorTitle = NSLocalizedString("Backup Error", comment: "")
|
||||||
|
self.errorFailure = description
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +143,9 @@ class BackupController: NSObject
|
|||||||
guard
|
guard
|
||||||
let altstoreAppGroup = Bundle.main.altstoreAppGroup,
|
let altstoreAppGroup = Bundle.main.altstoreAppGroup,
|
||||||
let sharedDirectoryURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: altstoreAppGroup)
|
let sharedDirectoryURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: altstoreAppGroup)
|
||||||
else { throw BackupError(.appGroupNotFound(nil), description: NSLocalizedString("Unable to create backup directory.", comment: "")) }
|
else {
|
||||||
|
throw BackupError(.appGroupNotFound(nil), description: NSLocalizedString("Unable to create backup directory.", comment: ""))
|
||||||
|
}
|
||||||
|
|
||||||
let backupsDirectory = sharedDirectoryURL.appendingPathComponent("Backups")
|
let backupsDirectory = sharedDirectoryURL.appendingPathComponent("Backups")
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>ALTAppGroups</key>
|
<key>ALTAppGroups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>group.com.rileytestut.AltStore</string>
|
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
||||||
</array>
|
</array>
|
||||||
<key>ALTBundleIdentifier</key>
|
<key>ALTBundleIdentifier</key>
|
||||||
<string>com.rileytestut.AltBackup</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
@@ -28,15 +28,15 @@
|
|||||||
<key>CFBundleTypeRole</key>
|
<key>CFBundleTypeRole</key>
|
||||||
<string>Editor</string>
|
<string>Editor</string>
|
||||||
<key>CFBundleURLName</key>
|
<key>CFBundleURLName</key>
|
||||||
<string>AltBackup General</string>
|
<string>SideBackup General</string>
|
||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>altbackup</string>
|
<string>sidebackup</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
|
|||||||
18
AltBackup/Resources/ReleaseEntitlements.plist
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>application-identifier</key>
|
||||||
|
<string>XYZ0123456.com.SideStore.SideStore.AltBackup</string>
|
||||||
|
<key>aps-environment</key>
|
||||||
|
<string>development</string>
|
||||||
|
<key>com.apple.developer.team-identifier</key>
|
||||||
|
<string>XYZ0123456</string>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.com.SideStore.SideStore</string>
|
||||||
|
</array>
|
||||||
|
<key>get-task-allow</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -82,23 +82,25 @@ class ViewController: UIViewController
|
|||||||
self.activityIndicatorView.color = .altstoreText
|
self.activityIndicatorView.color = .altstoreText
|
||||||
self.activityIndicatorView.startAnimating()
|
self.activityIndicatorView.startAnimating()
|
||||||
|
|
||||||
#if DEBUG
|
// TODO: @mahee96: Disabled these backup/restore buttons in altbackup.app screen which were present for debugging purpose.
|
||||||
let button1 = UIButton(type: .system)
|
// Can find something useful for these later, but these are not required by this backup/restore app
|
||||||
button1.setTitle("Backup", for: .normal)
|
// #if DEBUG
|
||||||
button1.setTitleColor(.white, for: .normal)
|
// let button1 = UIButton(type: .system)
|
||||||
button1.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
|
// button1.setTitle("Backup", for: .normal)
|
||||||
button1.addTarget(self, action: #selector(ViewController.backup), for: .primaryActionTriggered)
|
// button1.setTitleColor(.white, for: .normal)
|
||||||
|
// button1.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
|
||||||
let button2 = UIButton(type: .system)
|
// button1.addTarget(self, action: #selector(ViewController.backup), for: .primaryActionTriggered)
|
||||||
button2.setTitle("Restore", for: .normal)
|
//
|
||||||
button2.setTitleColor(.white, for: .normal)
|
// let button2 = UIButton(type: .system)
|
||||||
button2.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
|
// button2.setTitle("Restore", for: .normal)
|
||||||
button2.addTarget(self, action: #selector(ViewController.restore), for: .primaryActionTriggered)
|
// button2.setTitleColor(.white, for: .normal)
|
||||||
|
// button2.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
|
||||||
let arrangedSubviews = [self.textLabel!, self.detailTextLabel!, self.activityIndicatorView!, button1, button2]
|
// button2.addTarget(self, action: #selector(ViewController.restore), for: .primaryActionTriggered)
|
||||||
#else
|
//
|
||||||
|
// let arrangedSubviews = [self.textLabel!, self.detailTextLabel!, self.activityIndicatorView!, button1, button2]
|
||||||
|
// #else
|
||||||
let arrangedSubviews = [self.textLabel!, self.detailTextLabel!, self.activityIndicatorView!]
|
let arrangedSubviews = [self.textLabel!, self.detailTextLabel!, self.activityIndicatorView!]
|
||||||
#endif
|
// #endif
|
||||||
|
|
||||||
let stackView = UIStackView(arrangedSubviews: arrangedSubviews)
|
let stackView = UIStackView(arrangedSubviews: arrangedSubviews)
|
||||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
@@ -155,12 +157,13 @@ private extension ViewController
|
|||||||
self.textLabel.text = NSLocalizedString("Restoring app data…", comment: "")
|
self.textLabel.text = NSLocalizedString("Restoring app data…", comment: "")
|
||||||
self.detailTextLabel.isHidden = true
|
self.detailTextLabel.isHidden = true
|
||||||
self.activityIndicatorView.startAnimating()
|
self.activityIndicatorView.startAnimating()
|
||||||
|
|
||||||
|
// TODO: @mahee96: This is pointless since, app going in bg/fg should still report its last operation properly
|
||||||
case .none:
|
case .none:
|
||||||
self.textLabel.text = String(format: NSLocalizedString("%@ is inactive.", comment: ""),
|
self.textLabel.text = String(format: NSLocalizedString("%@ is inactive.", comment: ""),
|
||||||
Bundle.main.appName ?? NSLocalizedString("App", comment: ""))
|
Bundle.main.appName ?? NSLocalizedString("App", comment: ""))
|
||||||
|
|
||||||
self.detailTextLabel.text = String(format: NSLocalizedString("Refresh %@ in AltStore to continue using it.", comment: ""),
|
self.detailTextLabel.text = String(format: NSLocalizedString("Refresh %@ in SideStore to continue using it.", comment: ""),
|
||||||
Bundle.main.appName ?? NSLocalizedString("this app", comment: ""))
|
Bundle.main.appName ?? NSLocalizedString("this app", comment: ""))
|
||||||
|
|
||||||
self.detailTextLabel.isHidden = false
|
self.detailTextLabel.isHidden = false
|
||||||
@@ -198,6 +201,9 @@ private extension ViewController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: @mahee96: This doesn't account cases where backup is too long and user switched to other apps
|
||||||
|
// Now the user has lost his progress since current operation was cancelled due to switch between FG and BG
|
||||||
|
// if this just the reset for enum such that UI stops showing progress circle, then this is fine!
|
||||||
@objc func didEnterBackground(_ notification: Notification)
|
@objc func didEnterBackground(_ notification: Notification)
|
||||||
{
|
{
|
||||||
// Reset UI once we've left app (but not before).
|
// Reset UI once we've left app (but not before).
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
//
|
|
||||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
|
|
||||||
// Shared
|
|
||||||
#import "ALTConstants.h"
|
|
||||||
#import "ALTConnection.h"
|
|
||||||
#import "NSError+ALTServerError.h"
|
|
||||||
#import "CFNotificationName+AltStore.h"
|
|
||||||
|
|
||||||
// libproc
|
|
||||||
int proc_pidpath(int pid, void * buffer, uint32_t buffersize);
|
|
||||||
|
|
||||||
// Security.framework
|
|
||||||
CF_ENUM(uint32_t) {
|
|
||||||
kSecCSInternalInformation = 1 << 0,
|
|
||||||
kSecCSSigningInformation = 1 << 1,
|
|
||||||
kSecCSRequirementInformation = 1 << 2,
|
|
||||||
kSecCSDynamicInformation = 1 << 3,
|
|
||||||
kSecCSContentInformation = 1 << 4,
|
|
||||||
kSecCSSkipResourceDirectory = 1 << 5,
|
|
||||||
kSecCSCalculateCMSDigest = 1 << 6,
|
|
||||||
};
|
|
||||||
|
|
||||||
OSStatus SecStaticCodeCreateWithPath(CFURLRef path, uint32_t flags, void ** __nonnull CF_RETURNS_RETAINED staticCode);
|
|
||||||
OSStatus SecCodeCopySigningInformation(void *code, uint32_t flags, CFDictionaryRef * __nonnull CF_RETURNS_RETAINED information);
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
|
||||||
|
|
||||||
@interface AKDevice : NSObject
|
|
||||||
|
|
||||||
@property (class, readonly) AKDevice *currentDevice;
|
|
||||||
|
|
||||||
@property (strong, readonly) NSString *serialNumber;
|
|
||||||
@property (strong, readonly) NSString *uniqueDeviceIdentifier;
|
|
||||||
@property (strong, readonly) NSString *serverFriendlyDescription;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
@interface AKAppleIDSession : NSObject
|
|
||||||
|
|
||||||
- (instancetype)initWithIdentifier:(NSString *)identifier;
|
|
||||||
|
|
||||||
- (NSDictionary<NSString *, NSString *> *)appleIDHeadersForRequest:(NSURLRequest *)request;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
@interface LSApplicationWorkspace : NSObject
|
|
||||||
|
|
||||||
@property (class, readonly) LSApplicationWorkspace *defaultWorkspace;
|
|
||||||
|
|
||||||
- (BOOL)installApplication:(NSURL *)fileURL withOptions:(nullable NSDictionary<NSString *, id> *)options error:(NSError *_Nullable *)error;
|
|
||||||
- (BOOL)uninstallApplication:(NSString *)bundleIdentifier withOptions:(nullable NSDictionary *)options;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>application-identifier</key>
|
|
||||||
<string>6XVY5G3U44.com.rileytestut.AltDaemon</string>
|
|
||||||
<key>get-task-allow</key>
|
|
||||||
<true/>
|
|
||||||
<key>platform-application</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.authkit.client.private</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.private.mobileinstall.allowedSPI</key>
|
|
||||||
<array>
|
|
||||||
<string>Install</string>
|
|
||||||
<string>Uninstall</string>
|
|
||||||
<string>InstallForLaunchServices</string>
|
|
||||||
<string>UninstallForLaunchServices</string>
|
|
||||||
<string>InstallLocalProvisioned</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
//
|
|
||||||
// AnisetteDataManager.swift
|
|
||||||
// AltDaemon
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 6/1/20.
|
|
||||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
import AltSign
|
|
||||||
|
|
||||||
private extension UserDefaults
|
|
||||||
{
|
|
||||||
@objc var localUserID: String? {
|
|
||||||
get { return self.string(forKey: #keyPath(UserDefaults.localUserID)) }
|
|
||||||
set { self.set(newValue, forKey: #keyPath(UserDefaults.localUserID)) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AnisetteDataManager
|
|
||||||
{
|
|
||||||
static let shared = AnisetteDataManager()
|
|
||||||
|
|
||||||
private let dateFormatter = ISO8601DateFormatter()
|
|
||||||
|
|
||||||
private init()
|
|
||||||
{
|
|
||||||
dlopen("/System/Library/PrivateFrameworks/AuthKit.framework/AuthKit", RTLD_NOW);
|
|
||||||
}
|
|
||||||
|
|
||||||
func requestAnisetteData() throws -> ALTAnisetteData
|
|
||||||
{
|
|
||||||
var request = URLRequest(url: URL(string: "https://developerservices2.apple.com/services/QH65B2/listTeams.action?clientId=XABBG36SBA")!)
|
|
||||||
request.httpMethod = "POST"
|
|
||||||
|
|
||||||
let akAppleIDSession = unsafeBitCast(NSClassFromString("AKAppleIDSession")!, to: AKAppleIDSession.Type.self)
|
|
||||||
let akDevice = unsafeBitCast(NSClassFromString("AKDevice")!, to: AKDevice.Type.self)
|
|
||||||
|
|
||||||
let session = akAppleIDSession.init(identifier: "com.apple.gs.xcode.auth")
|
|
||||||
let headers = session.appleIDHeaders(for: request)
|
|
||||||
|
|
||||||
let device = akDevice.current
|
|
||||||
let date = self.dateFormatter.date(from: headers["X-Apple-I-Client-Time"] ?? "") ?? Date()
|
|
||||||
|
|
||||||
var localUserID = UserDefaults.standard.localUserID
|
|
||||||
if localUserID == nil
|
|
||||||
{
|
|
||||||
localUserID = UUID().uuidString
|
|
||||||
UserDefaults.standard.localUserID = localUserID
|
|
||||||
}
|
|
||||||
|
|
||||||
let anisetteData = ALTAnisetteData(machineID: headers["X-Apple-I-MD-M"] ?? "",
|
|
||||||
oneTimePassword: headers["X-Apple-I-MD"] ?? "",
|
|
||||||
localUserID: headers["X-Apple-I-MD-LU"] ?? localUserID ?? "",
|
|
||||||
routingInfo: UInt64(headers["X-Apple-I-MD-RINFO"] ?? "") ?? 0,
|
|
||||||
deviceUniqueIdentifier: device.uniqueDeviceIdentifier,
|
|
||||||
deviceSerialNumber: device.serialNumber,
|
|
||||||
deviceDescription: "<MacBookPro15,1> <Mac OS X;10.15.2;19C57> <com.apple.AuthKit/1 (com.apple.dt.Xcode/3594.4.19)>",
|
|
||||||
date: date,
|
|
||||||
locale: .current,
|
|
||||||
timeZone: .current)
|
|
||||||
return anisetteData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
//
|
|
||||||
// AppManager.swift
|
|
||||||
// AltDaemon
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 6/1/20.
|
|
||||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
import AltSign
|
|
||||||
|
|
||||||
private extension URL
|
|
||||||
{
|
|
||||||
static let profilesDirectoryURL = URL(fileURLWithPath: "/var/MobileDevice/ProvisioningProfiles", isDirectory: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension CFNotificationName
|
|
||||||
{
|
|
||||||
static let updatedProvisioningProfiles = CFNotificationName("MISProvisioningProfileRemoved" as CFString)
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AppManager
|
|
||||||
{
|
|
||||||
static let shared = AppManager()
|
|
||||||
|
|
||||||
private let appQueue = DispatchQueue(label: "com.rileytestut.AltDaemon.appQueue", qos: .userInitiated)
|
|
||||||
private let profilesQueue = OperationQueue()
|
|
||||||
|
|
||||||
private let fileCoordinator = NSFileCoordinator()
|
|
||||||
|
|
||||||
private init()
|
|
||||||
{
|
|
||||||
self.profilesQueue.name = "com.rileytestut.AltDaemon.profilesQueue"
|
|
||||||
self.profilesQueue.qualityOfService = .userInitiated
|
|
||||||
}
|
|
||||||
|
|
||||||
func installApp(at fileURL: URL, bundleIdentifier: String, activeProfiles: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
|
||||||
{
|
|
||||||
self.appQueue.async {
|
|
||||||
let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self)
|
|
||||||
|
|
||||||
let options = ["CFBundleIdentifier": bundleIdentifier, "AllowInstallLocalProvisioned": NSNumber(value: true)] as [String : Any]
|
|
||||||
let result = Result { try lsApplicationWorkspace.default.installApplication(fileURL, withOptions: options) }
|
|
||||||
|
|
||||||
completionHandler(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeApp(forBundleIdentifier bundleIdentifier: String, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
|
||||||
{
|
|
||||||
self.appQueue.async {
|
|
||||||
let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self)
|
|
||||||
lsApplicationWorkspace.default.uninstallApplication(bundleIdentifier, withOptions: nil)
|
|
||||||
|
|
||||||
completionHandler(.success(()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func install(_ profiles: Set<ALTProvisioningProfile>, activeProfiles: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
|
||||||
{
|
|
||||||
let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: [])
|
|
||||||
self.fileCoordinator.coordinate(with: [intent], queue: self.profilesQueue) { (error) in
|
|
||||||
do
|
|
||||||
{
|
|
||||||
if let error = error
|
|
||||||
{
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
let installingBundleIDs = Set(profiles.map(\.bundleIdentifier))
|
|
||||||
|
|
||||||
let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: [])
|
|
||||||
|
|
||||||
// Remove all inactive profiles (if active profiles are provided), and the previous profiles.
|
|
||||||
for fileURL in profileURLs
|
|
||||||
{
|
|
||||||
// Use memory mapping to reduce peak memory usage and stay within limit.
|
|
||||||
guard let profile = try? ALTProvisioningProfile(url: fileURL, options: [.mappedIfSafe]) else { continue }
|
|
||||||
|
|
||||||
if installingBundleIDs.contains(profile.bundleIdentifier) || (activeProfiles?.contains(profile.bundleIdentifier) == false && profile.isFreeProvisioningProfile)
|
|
||||||
{
|
|
||||||
try FileManager.default.removeItem(at: fileURL)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
print("Ignoring:", profile.bundleIdentifier, profile.uuid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for profile in profiles
|
|
||||||
{
|
|
||||||
let destinationURL = URL.profilesDirectoryURL.appendingPathComponent(profile.uuid.uuidString.lowercased())
|
|
||||||
try profile.data.write(to: destinationURL, options: .atomic)
|
|
||||||
}
|
|
||||||
|
|
||||||
completionHandler(.success(()))
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
completionHandler(.failure(error))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notify system to prevent accidentally untrusting developer certificate.
|
|
||||||
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .updatedProvisioningProfiles, nil, nil, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeProvisioningProfiles(forBundleIdentifiers bundleIdentifiers: Set<String>, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
|
||||||
{
|
|
||||||
let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: [])
|
|
||||||
self.fileCoordinator.coordinate(with: [intent], queue: self.profilesQueue) { (error) in
|
|
||||||
do
|
|
||||||
{
|
|
||||||
let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: [])
|
|
||||||
|
|
||||||
for fileURL in profileURLs
|
|
||||||
{
|
|
||||||
guard let profile = ALTProvisioningProfile(url: fileURL) else { continue }
|
|
||||||
|
|
||||||
if bundleIdentifiers.contains(profile.bundleIdentifier)
|
|
||||||
{
|
|
||||||
try FileManager.default.removeItem(at: fileURL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
completionHandler(.success(()))
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
completionHandler(.failure(error))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notify system to prevent accidentally untrusting developer certificate.
|
|
||||||
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .updatedProvisioningProfiles, nil, nil, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
//
|
|
||||||
// DaemonRequestHandler.swift
|
|
||||||
// AltDaemon
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 6/1/20.
|
|
||||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
typealias DaemonConnectionManager = ConnectionManager<DaemonRequestHandler>
|
|
||||||
|
|
||||||
private let connectionManager = ConnectionManager(requestHandler: DaemonRequestHandler(),
|
|
||||||
connectionHandlers: [XPCConnectionHandler()])
|
|
||||||
|
|
||||||
extension DaemonConnectionManager
|
|
||||||
{
|
|
||||||
static var shared: ConnectionManager {
|
|
||||||
return connectionManager
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DaemonRequestHandler: RequestHandler
|
|
||||||
{
|
|
||||||
func handleAnisetteDataRequest(_ request: AnisetteDataRequest, for connection: Connection, completionHandler: @escaping (Result<AnisetteDataResponse, Error>) -> Void)
|
|
||||||
{
|
|
||||||
do
|
|
||||||
{
|
|
||||||
let anisetteData = try AnisetteDataManager.shared.requestAnisetteData()
|
|
||||||
|
|
||||||
let response = AnisetteDataResponse(anisetteData: anisetteData)
|
|
||||||
completionHandler(.success(response))
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
completionHandler(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: Connection, completionHandler: @escaping (Result<InstallationProgressResponse, Error>) -> Void)
|
|
||||||
{
|
|
||||||
guard let fileURL = request.fileURL else { return completionHandler(.failure(ALTServerError(.invalidRequest))) }
|
|
||||||
|
|
||||||
print("Awaiting begin installation request...")
|
|
||||||
|
|
||||||
connection.receiveRequest() { (result) in
|
|
||||||
print("Received begin installation request with result:", result)
|
|
||||||
|
|
||||||
do
|
|
||||||
{
|
|
||||||
guard case .beginInstallation(let request) = try result.get() else { throw ALTServerError(.unknownRequest) }
|
|
||||||
guard let bundleIdentifier = request.bundleIdentifier else { throw ALTServerError(.invalidRequest) }
|
|
||||||
|
|
||||||
AppManager.shared.installApp(at: fileURL, bundleIdentifier: bundleIdentifier, activeProfiles: request.activeProfiles) { (result) in
|
|
||||||
let result = result.map { InstallationProgressResponse(progress: 1.0) }
|
|
||||||
print("Installed app with result:", result)
|
|
||||||
|
|
||||||
completionHandler(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
completionHandler(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleInstallProvisioningProfilesRequest(_ request: InstallProvisioningProfilesRequest, for connection: Connection,
|
|
||||||
completionHandler: @escaping (Result<InstallProvisioningProfilesResponse, Error>) -> Void)
|
|
||||||
{
|
|
||||||
AppManager.shared.install(request.provisioningProfiles, activeProfiles: request.activeProfiles) { (result) in
|
|
||||||
switch result
|
|
||||||
{
|
|
||||||
case .failure(let error):
|
|
||||||
print("Failed to install profiles \(request.provisioningProfiles.map { $0.bundleIdentifier }):", error)
|
|
||||||
completionHandler(.failure(error))
|
|
||||||
|
|
||||||
case .success:
|
|
||||||
print("Installed profiles:", request.provisioningProfiles.map { $0.bundleIdentifier })
|
|
||||||
|
|
||||||
let response = InstallProvisioningProfilesResponse()
|
|
||||||
completionHandler(.success(response))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleRemoveProvisioningProfilesRequest(_ request: RemoveProvisioningProfilesRequest, for connection: Connection,
|
|
||||||
completionHandler: @escaping (Result<RemoveProvisioningProfilesResponse, Error>) -> Void)
|
|
||||||
{
|
|
||||||
AppManager.shared.removeProvisioningProfiles(forBundleIdentifiers: request.bundleIdentifiers) { (result) in
|
|
||||||
switch result
|
|
||||||
{
|
|
||||||
case .failure(let error):
|
|
||||||
print("Failed to remove profiles \(request.bundleIdentifiers):", error)
|
|
||||||
completionHandler(.failure(error))
|
|
||||||
|
|
||||||
case .success:
|
|
||||||
print("Removed profiles:", request.bundleIdentifiers)
|
|
||||||
|
|
||||||
let response = RemoveProvisioningProfilesResponse()
|
|
||||||
completionHandler(.success(response))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleRemoveAppRequest(_ request: RemoveAppRequest, for connection: Connection, completionHandler: @escaping (Result<RemoveAppResponse, Error>) -> Void)
|
|
||||||
{
|
|
||||||
AppManager.shared.removeApp(forBundleIdentifier: request.bundleIdentifier) { (result) in
|
|
||||||
switch result
|
|
||||||
{
|
|
||||||
case .failure(let error):
|
|
||||||
print("Failed to remove app \(request.bundleIdentifier):", error)
|
|
||||||
completionHandler(.failure(error))
|
|
||||||
|
|
||||||
case .success:
|
|
||||||
print("Removed app:", request.bundleIdentifier)
|
|
||||||
|
|
||||||
let response = RemoveAppResponse()
|
|
||||||
completionHandler(.success(response))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
//
|
|
||||||
// XPCConnectionHandler.swift
|
|
||||||
// AltDaemon
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 9/14/20.
|
|
||||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Security
|
|
||||||
|
|
||||||
class XPCConnectionHandler: NSObject, ConnectionHandler
|
|
||||||
{
|
|
||||||
var connectionHandler: ((Connection) -> Void)?
|
|
||||||
var disconnectionHandler: ((Connection) -> Void)?
|
|
||||||
|
|
||||||
private let dispatchQueue = DispatchQueue(label: "io.altstore.XPCConnectionListener", qos: .utility)
|
|
||||||
private let listeners = XPCConnection.machServiceNames.map { NSXPCListener.makeListener(machServiceName: $0) }
|
|
||||||
|
|
||||||
deinit
|
|
||||||
{
|
|
||||||
self.stopListening()
|
|
||||||
}
|
|
||||||
|
|
||||||
func startListening()
|
|
||||||
{
|
|
||||||
for listener in self.listeners
|
|
||||||
{
|
|
||||||
listener.delegate = self
|
|
||||||
listener.resume()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopListening()
|
|
||||||
{
|
|
||||||
self.listeners.forEach { $0.suspend() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension XPCConnectionHandler
|
|
||||||
{
|
|
||||||
func disconnect(_ connection: Connection)
|
|
||||||
{
|
|
||||||
connection.disconnect()
|
|
||||||
|
|
||||||
self.disconnectionHandler?(connection)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension XPCConnectionHandler: NSXPCListenerDelegate
|
|
||||||
{
|
|
||||||
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool
|
|
||||||
{
|
|
||||||
let maximumPathLength = 4 * UInt32(MAXPATHLEN)
|
|
||||||
|
|
||||||
let pathBuffer = UnsafeMutablePointer<CChar>.allocate(capacity: Int(maximumPathLength))
|
|
||||||
defer { pathBuffer.deallocate() }
|
|
||||||
|
|
||||||
proc_pidpath(newConnection.processIdentifier, pathBuffer, maximumPathLength)
|
|
||||||
|
|
||||||
let path = String(cString: pathBuffer)
|
|
||||||
let fileURL = URL(fileURLWithPath: path)
|
|
||||||
|
|
||||||
var code: UnsafeMutableRawPointer?
|
|
||||||
defer { code.map { Unmanaged<AnyObject>.fromOpaque($0).release() } }
|
|
||||||
|
|
||||||
var status = SecStaticCodeCreateWithPath(fileURL as CFURL, 0, &code)
|
|
||||||
guard status == 0 else { return false }
|
|
||||||
|
|
||||||
var signingInfo: CFDictionary?
|
|
||||||
defer { signingInfo.map { Unmanaged<AnyObject>.passUnretained($0).release() } }
|
|
||||||
|
|
||||||
status = SecCodeCopySigningInformation(code, kSecCSInternalInformation | kSecCSSigningInformation, &signingInfo)
|
|
||||||
guard status == 0 else { return false }
|
|
||||||
|
|
||||||
// Only accept connections from AltStore.
|
|
||||||
guard
|
|
||||||
let codeSigningInfo = signingInfo as? [String: Any],
|
|
||||||
let bundleIdentifier = codeSigningInfo["identifier"] as? String,
|
|
||||||
bundleIdentifier.contains("com.rileytestut.AltStore")
|
|
||||||
else { return false }
|
|
||||||
|
|
||||||
let connection = XPCConnection(newConnection)
|
|
||||||
newConnection.invalidationHandler = { [weak self, weak connection] in
|
|
||||||
guard let self = self, let connection = connection else { return }
|
|
||||||
self.disconnect(connection)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.connectionHandler?(connection)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
//
|
|
||||||
// main.swift
|
|
||||||
// AltDaemon
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 6/2/20.
|
|
||||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
autoreleasepool {
|
|
||||||
DaemonConnectionManager.shared.start()
|
|
||||||
RunLoop.current.run()
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
Package: com.rileytestut.altdaemon
|
|
||||||
Name: AltDaemon
|
|
||||||
Depends:
|
|
||||||
Version: 1.0
|
|
||||||
Architecture: iphoneos-arm
|
|
||||||
Description: AltDaemon allows AltStore to install and refresh apps without a computer.
|
|
||||||
Maintainer: Riley Testut
|
|
||||||
Author: Riley Testut
|
|
||||||
Homepage: https://altstore.io
|
|
||||||
Section: System
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
launchctl load /Library/LaunchDaemons/com.rileytestut.altdaemon.plist
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
launchctl unload /Library/LaunchDaemons/com.rileytestut.altdaemon.plist >> /dev/null 2>&1
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
launchctl unload /Library/LaunchDaemons/com.rileytestut.altdaemon.plist
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>Label</key>
|
|
||||||
<string>com.rileytestut.altdaemon</string>
|
|
||||||
<key>ProgramArguments</key>
|
|
||||||
<array>
|
|
||||||
<string>/usr/bin/env</string>
|
|
||||||
<string>_MSSafeMode=1</string>
|
|
||||||
<string>_SafeMode=1</string>
|
|
||||||
<string>/usr/bin/AltDaemon</string>
|
|
||||||
</array>
|
|
||||||
<key>UserName</key>
|
|
||||||
<string>mobile</string>
|
|
||||||
<key>KeepAlive</key>
|
|
||||||
<false/>
|
|
||||||
<key>RunAtLoad</key>
|
|
||||||
<false/>
|
|
||||||
<key>MachServices</key>
|
|
||||||
<dict>
|
|
||||||
<key>cy:io.altstore.altdaemon</key>
|
|
||||||
<true/>
|
|
||||||
<key>lh:io.altstore.altdaemon</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
//
|
|
||||||
// ALTPluginService.h
|
|
||||||
// AltPlugin
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 11/14/19.
|
|
||||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
|
|
||||||
@class ALTAnisetteData;
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
|
||||||
|
|
||||||
@interface ALTPluginService : NSObject
|
|
||||||
|
|
||||||
@property (class, nonatomic, readonly) ALTPluginService *sharedService;
|
|
||||||
|
|
||||||
- (ALTAnisetteData *)requestAnisetteData;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
//
|
|
||||||
// ALTPluginService.m
|
|
||||||
// AltPlugin
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 11/14/19.
|
|
||||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "ALTPluginService.h"
|
|
||||||
|
|
||||||
#import <dlfcn.h>
|
|
||||||
|
|
||||||
#import "ALTAnisetteData.h"
|
|
||||||
|
|
||||||
@import AppKit;
|
|
||||||
|
|
||||||
@interface AKAppleIDSession : NSObject
|
|
||||||
- (id)appleIDHeadersForRequest:(id)arg1;
|
|
||||||
@end
|
|
||||||
|
|
||||||
@interface AKDevice
|
|
||||||
+ (AKDevice *)currentDevice;
|
|
||||||
- (NSString *)uniqueDeviceIdentifier;
|
|
||||||
- (nullable NSString *)serialNumber;
|
|
||||||
- (NSString *)serverFriendlyDescription;
|
|
||||||
@end
|
|
||||||
|
|
||||||
@interface ALTPluginService ()
|
|
||||||
|
|
||||||
@property (nonatomic, readonly) NSISO8601DateFormatter *dateFormatter;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation ALTPluginService
|
|
||||||
|
|
||||||
+ (instancetype)sharedService
|
|
||||||
{
|
|
||||||
static ALTPluginService *_service = nil;
|
|
||||||
static dispatch_once_t onceToken;
|
|
||||||
dispatch_once(&onceToken, ^{
|
|
||||||
_service = [[self alloc] init];
|
|
||||||
});
|
|
||||||
|
|
||||||
return _service;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (instancetype)init
|
|
||||||
{
|
|
||||||
self = [super init];
|
|
||||||
if (self)
|
|
||||||
{
|
|
||||||
_dateFormatter = [[NSISO8601DateFormatter alloc] init];
|
|
||||||
}
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
+ (void)initialize
|
|
||||||
{
|
|
||||||
[[ALTPluginService sharedService] start];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)start
|
|
||||||
{
|
|
||||||
dlopen("/System/Library/PrivateFrameworks/AuthKit.framework/AuthKit", RTLD_NOW);
|
|
||||||
|
|
||||||
[[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(receiveNotification:) name:@"com.rileytestut.AltServer.FetchAnisetteData" object:nil];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (ALTAnisetteData *)requestAnisetteData
|
|
||||||
{
|
|
||||||
NSMutableURLRequest* req = [[NSMutableURLRequest alloc] initWithURL:[[NSURL alloc] initWithString:@"https://developerservices2.apple.com/services/QH65B2/listTeams.action?clientId=XABBG36SBA"]];
|
|
||||||
[req setHTTPMethod:@"POST"];
|
|
||||||
|
|
||||||
AKAppleIDSession *session = [[NSClassFromString(@"AKAppleIDSession") alloc] initWithIdentifier:@"com.apple.gs.xcode.auth"];
|
|
||||||
NSDictionary *headers = [session appleIDHeadersForRequest:req];
|
|
||||||
|
|
||||||
AKDevice *device = [NSClassFromString(@"AKDevice") currentDevice];
|
|
||||||
NSDate *date = [self.dateFormatter dateFromString:headers[@"X-Apple-I-Client-Time"]];
|
|
||||||
|
|
||||||
ALTAnisetteData *anisetteData = [[NSClassFromString(@"ALTAnisetteData") alloc] initWithMachineID:headers[@"X-Apple-I-MD-M"]
|
|
||||||
oneTimePassword:headers[@"X-Apple-I-MD"]
|
|
||||||
localUserID:headers[@"X-Apple-I-MD-LU"]
|
|
||||||
routingInfo:[headers[@"X-Apple-I-MD-RINFO"] longLongValue]
|
|
||||||
deviceUniqueIdentifier:device.uniqueDeviceIdentifier
|
|
||||||
deviceSerialNumber:device.serialNumber ?: @"C02LKHBBFD57" // serialNumber can be nil, so provide valid fallback serial number.
|
|
||||||
deviceDescription:device.serverFriendlyDescription
|
|
||||||
date:date
|
|
||||||
locale:[NSLocale currentLocale]
|
|
||||||
timeZone:[NSTimeZone localTimeZone]];
|
|
||||||
|
|
||||||
return anisetteData;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)receiveNotification:(NSNotification *)notification
|
|
||||||
{
|
|
||||||
NSString *requestUUID = notification.userInfo[@"requestUUID"];
|
|
||||||
|
|
||||||
ALTAnisetteData *anisetteData = [self requestAnisetteData];
|
|
||||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:anisetteData requiringSecureCoding:YES error:nil];
|
|
||||||
|
|
||||||
[[NSDistributedNotificationCenter defaultCenter] postNotificationName:@"com.rileytestut.AltServer.AnisetteDataResponse" object:nil userInfo:@{@"requestUUID": requestUUID, @"anisetteData": data} deliverImmediately:YES];
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
|
||||||
<key>CFBundleExecutable</key>
|
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
|
||||||
<key>CFBundleIdentifier</key>
|
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
|
||||||
<string>6.0</string>
|
|
||||||
<key>CFBundleName</key>
|
|
||||||
<string>$(PRODUCT_NAME)</string>
|
|
||||||
<key>CFBundlePackageType</key>
|
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
|
||||||
<key>CFBundleShortVersionString</key>
|
|
||||||
<string>$(MARKETING_VERSION)</string>
|
|
||||||
<key>CFBundleVersion</key>
|
|
||||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
|
||||||
<key>NSHumanReadableCopyright</key>
|
|
||||||
<string>Copyright © 2019 Riley Testut. All rights reserved.</string>
|
|
||||||
<key>NSPrincipalClass</key>
|
|
||||||
<string>ALTPluginService</string>
|
|
||||||
<key>Supported10.14PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string># UUIDs for versions from 10.12 to 99.99.99</string>
|
|
||||||
<string># For mail version 10.0 (3226) on OS X Version 10.12 (build 16A319)</string>
|
|
||||||
<string>36CCB8BB-2207-455E-89BC-B9D6E47ABB5B</string>
|
|
||||||
<string># For mail version 10.1 (3251) on OS X Version 10.12.1 (build 16B2553a)</string>
|
|
||||||
<string>9054AFD9-2607-489E-8E63-8B09A749BC61</string>
|
|
||||||
<string># For mail version 10.2 (3259) on OS X Version 10.12.2 (build 16D12b)</string>
|
|
||||||
<string>1CD3B36A-0E3B-4A26-8F7E-5BDF96AAC97E</string>
|
|
||||||
<string># For mail version 10.3 (3273) on OS X Version 10.12.4 (build 16G1036)</string>
|
|
||||||
<string>21560BD9-A3CC-482E-9B99-95B7BF61EDC1</string>
|
|
||||||
<string># For mail version 11.0 (3441.0.1) on OS X Version 10.13 (build 17A315i)</string>
|
|
||||||
<string>C86CD990-4660-4E36-8CDA-7454DEB2E199</string>
|
|
||||||
<string># For mail version 12.0 (3445.100.39) on OS X Version 10.14.1 (build 18B45d)</string>
|
|
||||||
<string>A4343FAF-AE18-40D0-8A16-DFAE481AF9C1</string>
|
|
||||||
<string># For mail version 13.0 (3594.4.2) on OS X Version 10.15 (build 19A558d)</string>
|
|
||||||
<string>6EEA38FB-1A0B-469B-BB35-4C2E0EEA9053</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported10.15PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string># UUIDs for versions from 10.12 to 99.99.99</string>
|
|
||||||
<string># For mail version 10.0 (3226) on OS X Version 10.12 (build 16A319)</string>
|
|
||||||
<string>36CCB8BB-2207-455E-89BC-B9D6E47ABB5B</string>
|
|
||||||
<string># For mail version 10.1 (3251) on OS X Version 10.12.1 (build 16B2553a)</string>
|
|
||||||
<string>9054AFD9-2607-489E-8E63-8B09A749BC61</string>
|
|
||||||
<string># For mail version 10.2 (3259) on OS X Version 10.12.2 (build 16D12b)</string>
|
|
||||||
<string>1CD3B36A-0E3B-4A26-8F7E-5BDF96AAC97E</string>
|
|
||||||
<string># For mail version 10.3 (3273) on OS X Version 10.12.4 (build 16G1036)</string>
|
|
||||||
<string>21560BD9-A3CC-482E-9B99-95B7BF61EDC1</string>
|
|
||||||
<string># For mail version 11.0 (3441.0.1) on OS X Version 10.13 (build 17A315i)</string>
|
|
||||||
<string>C86CD990-4660-4E36-8CDA-7454DEB2E199</string>
|
|
||||||
<string># For mail version 12.0 (3445.100.39) on OS X Version 10.14.1 (build 18B45d)</string>
|
|
||||||
<string>A4343FAF-AE18-40D0-8A16-DFAE481AF9C1</string>
|
|
||||||
<string># For mail version 13.0 (3594.4.2) on OS X Version 10.15 (build 19A558d)</string>
|
|
||||||
<string>6EEA38FB-1A0B-469B-BB35-4C2E0EEA9053</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported11.0PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported11.10PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported11.1PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported11.2PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported11.3PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported11.4PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported11.5PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported11.6PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported11.7PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported11.8PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported11.9PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported12.0PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
|
||||||
<string>224E7F96-2099-499C-A501-63FB68C79CD2</string>
|
|
||||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported12.1PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
|
||||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported12.2PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
|
||||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported12.3PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
|
||||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
|
||||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported12.4PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
|
||||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
|
||||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported12.5PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
|
||||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
|
||||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported12.6PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
|
||||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
|
||||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported12.7PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
|
||||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
|
||||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported12.8PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
|
||||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
|
||||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported12.9PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
|
||||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
|
||||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported13.0PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
|
||||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
|
||||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
|
||||||
<string>890E3F5B-9490-4828-8F3F-B6561E513FCC</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported13.1PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>890E3F5B-9490-4828-8F3F-B6561E513FCC</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported13.2PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>890E3F5B-9490-4828-8F3F-B6561E513FCC</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported13.3PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>890E3F5B-9490-4828-8F3F-B6561E513FCC</string>
|
|
||||||
<string>281F8A5C-0AF9-4BE6-8B8A-C0CB9C2068BE</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported13.4PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>890E3F5B-9490-4828-8F3F-B6561E513FCC</string>
|
|
||||||
<string>281F8A5C-0AF9-4BE6-8B8A-C0CB9C2068BE</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported13.5PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>890E3F5B-9490-4828-8F3F-B6561E513FCC</string>
|
|
||||||
<string>281F8A5C-0AF9-4BE6-8B8A-C0CB9C2068BE</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported13.6PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>890E3F5B-9490-4828-8F3F-B6561E513FCC</string>
|
|
||||||
<string>281F8A5C-0AF9-4BE6-8B8A-C0CB9C2068BE</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported13.7PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>890E3F5B-9490-4828-8F3F-B6561E513FCC</string>
|
|
||||||
<string>281F8A5C-0AF9-4BE6-8B8A-C0CB9C2068BE</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported13.8PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>890E3F5B-9490-4828-8F3F-B6561E513FCC</string>
|
|
||||||
<string>281F8A5C-0AF9-4BE6-8B8A-C0CB9C2068BE</string>
|
|
||||||
</array>
|
|
||||||
<key>Supported13.9PluginCompatibilityUUIDs</key>
|
|
||||||
<array>
|
|
||||||
<string>890E3F5B-9490-4828-8F3F-B6561E513FCC</string>
|
|
||||||
<string>281F8A5C-0AF9-4BE6-8B8A-C0CB9C2068BE</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
//
|
|
||||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "ALTDeviceManager.h"
|
|
||||||
#import "ALTWiredConnection.h"
|
|
||||||
#import "ALTNotificationConnection.h"
|
|
||||||
#import "ALTDebugConnection.h"
|
|
||||||
|
|
||||||
// Shared
|
|
||||||
#import "ALTConstants.h"
|
|
||||||
#import "ALTConnection.h"
|
|
||||||
#import "AltXPCProtocol.h"
|
|
||||||
#import "ALTWrappedError.h"
|
|
||||||
#import "NSError+ALTServerError.h"
|
|
||||||
#import "CFNotificationName+AltStore.h"
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict/>
|
|
||||||
</plist>
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
//
|
|
||||||
// AnisetteDataManager.swift
|
|
||||||
// AltServer
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 11/16/19.
|
|
||||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
private extension Bundle
|
|
||||||
{
|
|
||||||
struct ID
|
|
||||||
{
|
|
||||||
static let mail = "com.apple.mail"
|
|
||||||
static let altXPC = "com.rileytestut.AltXPC"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension ALTAnisetteData
|
|
||||||
{
|
|
||||||
func sanitize(byReplacingBundleID bundleID: String)
|
|
||||||
{
|
|
||||||
guard let range = self.deviceDescription.lowercased().range(of: "(" + bundleID.lowercased()) else { return }
|
|
||||||
|
|
||||||
var adjustedDescription = self.deviceDescription[..<range.lowerBound]
|
|
||||||
adjustedDescription += "(com.apple.dt.Xcode/3594.4.19)>"
|
|
||||||
|
|
||||||
self.deviceDescription = String(adjustedDescription)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AnisetteDataManager: NSObject
|
|
||||||
{
|
|
||||||
static let shared = AnisetteDataManager()
|
|
||||||
|
|
||||||
private var anisetteDataCompletionHandlers: [String: (Result<ALTAnisetteData, Error>) -> Void] = [:]
|
|
||||||
private var anisetteDataTimers: [String: Timer] = [:]
|
|
||||||
|
|
||||||
private lazy var xpcConnection: NSXPCConnection = {
|
|
||||||
let connection = NSXPCConnection(serviceName: Bundle.ID.altXPC)
|
|
||||||
connection.remoteObjectInterface = NSXPCInterface(with: AltXPCProtocol.self)
|
|
||||||
connection.resume()
|
|
||||||
return connection
|
|
||||||
}()
|
|
||||||
|
|
||||||
private override init()
|
|
||||||
{
|
|
||||||
super.init()
|
|
||||||
|
|
||||||
DistributedNotificationCenter.default().addObserver(self, selector: #selector(AnisetteDataManager.handleAnisetteDataResponse(_:)), name: Notification.Name("com.rileytestut.AltServer.AnisetteDataResponse"), object: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func requestAnisetteData(_ completion: @escaping (Result<ALTAnisetteData, Error>) -> Void)
|
|
||||||
{
|
|
||||||
if #available(macOS 10.15, *)
|
|
||||||
{
|
|
||||||
self.requestAnisetteDataFromXPCService { (result) in
|
|
||||||
do
|
|
||||||
{
|
|
||||||
let anisetteData = try result.get()
|
|
||||||
completion(.success(anisetteData))
|
|
||||||
}
|
|
||||||
catch CocoaError.xpcConnectionInterrupted
|
|
||||||
{
|
|
||||||
// SIP and/or AMFI are not disabled, so fall back to Mail plug-in.
|
|
||||||
self.requestAnisetteDataFromPlugin { (result) in
|
|
||||||
completion(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
self.requestAnisetteDataFromPlugin { (result) in
|
|
||||||
completion(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isXPCAvailable(completion: @escaping (Bool) -> Void)
|
|
||||||
{
|
|
||||||
guard let proxy = self.xpcConnection.remoteObjectProxyWithErrorHandler({ (error) in
|
|
||||||
completion(false)
|
|
||||||
}) as? AltXPCProtocol else { return }
|
|
||||||
|
|
||||||
proxy.ping {
|
|
||||||
completion(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension AnisetteDataManager
|
|
||||||
{
|
|
||||||
@available(macOS 10.15, *)
|
|
||||||
func requestAnisetteDataFromXPCService(completion: @escaping (Result<ALTAnisetteData, Error>) -> Void)
|
|
||||||
{
|
|
||||||
guard let proxy = self.xpcConnection.remoteObjectProxyWithErrorHandler({ (error) in
|
|
||||||
print("Anisette XPC Error:", error)
|
|
||||||
completion(.failure(error))
|
|
||||||
}) as? AltXPCProtocol else { return }
|
|
||||||
|
|
||||||
proxy.requestAnisetteData { (anisetteData, error) in
|
|
||||||
anisetteData?.sanitize(byReplacingBundleID: Bundle.ID.altXPC)
|
|
||||||
completion(Result(anisetteData, error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func requestAnisetteDataFromPlugin(completion: @escaping (Result<ALTAnisetteData, Error>) -> Void)
|
|
||||||
{
|
|
||||||
let requestUUID = UUID().uuidString
|
|
||||||
self.anisetteDataCompletionHandlers[requestUUID] = completion
|
|
||||||
|
|
||||||
let timer = Timer(timeInterval: 1.0, repeats: false) { (timer) in
|
|
||||||
self.finishRequest(forUUID: requestUUID, result: .failure(ALTServerError(.pluginNotFound)))
|
|
||||||
}
|
|
||||||
self.anisetteDataTimers[requestUUID] = timer
|
|
||||||
|
|
||||||
RunLoop.main.add(timer, forMode: .default)
|
|
||||||
|
|
||||||
DistributedNotificationCenter.default().postNotificationName(Notification.Name("com.rileytestut.AltServer.FetchAnisetteData"), object: nil, userInfo: ["requestUUID": requestUUID], options: .deliverImmediately)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func handleAnisetteDataResponse(_ notification: Notification)
|
|
||||||
{
|
|
||||||
guard let userInfo = notification.userInfo, let requestUUID = userInfo["requestUUID"] as? String else { return }
|
|
||||||
|
|
||||||
if
|
|
||||||
let archivedAnisetteData = userInfo["anisetteData"] as? Data,
|
|
||||||
let anisetteData = try? NSKeyedUnarchiver.unarchivedObject(ofClass: ALTAnisetteData.self, from: archivedAnisetteData)
|
|
||||||
{
|
|
||||||
anisetteData.sanitize(byReplacingBundleID: Bundle.ID.mail)
|
|
||||||
self.finishRequest(forUUID: requestUUID, result: .success(anisetteData))
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
self.finishRequest(forUUID: requestUUID, result: .failure(ALTServerError(.invalidAnisetteData)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func finishRequest(forUUID requestUUID: String, result: Result<ALTAnisetteData, Error>)
|
|
||||||
{
|
|
||||||
let completionHandler = self.anisetteDataCompletionHandlers[requestUUID]
|
|
||||||
self.anisetteDataCompletionHandlers[requestUUID] = nil
|
|
||||||
|
|
||||||
let timer = self.anisetteDataTimers[requestUUID]
|
|
||||||
self.anisetteDataTimers[requestUUID] = nil
|
|
||||||
|
|
||||||
timer?.invalidate()
|
|
||||||
completionHandler?(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,586 +0,0 @@
|
|||||||
//
|
|
||||||
// AppDelegate.swift
|
|
||||||
// AltServer
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 5/24/19.
|
|
||||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Cocoa
|
|
||||||
import UserNotifications
|
|
||||||
|
|
||||||
import AltSign
|
|
||||||
|
|
||||||
import LaunchAtLogin
|
|
||||||
import Sparkle
|
|
||||||
|
|
||||||
extension ALTDevice: MenuDisplayable {}
|
|
||||||
|
|
||||||
@NSApplicationMain
|
|
||||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
|
||||||
|
|
||||||
private let pluginManager = PluginManager()
|
|
||||||
|
|
||||||
private var statusItem: NSStatusItem?
|
|
||||||
|
|
||||||
private var connectedDevices = [ALTDevice]()
|
|
||||||
|
|
||||||
private weak var authenticationAlert: NSAlert?
|
|
||||||
|
|
||||||
@IBOutlet private var appMenu: NSMenu!
|
|
||||||
@IBOutlet private var connectedDevicesMenu: NSMenu!
|
|
||||||
@IBOutlet private var sideloadIPAConnectedDevicesMenu: NSMenu!
|
|
||||||
@IBOutlet private var enableJITMenu: NSMenu!
|
|
||||||
|
|
||||||
@IBOutlet private var launchAtLoginMenuItem: NSMenuItem!
|
|
||||||
@IBOutlet private var installMailPluginMenuItem: NSMenuItem!
|
|
||||||
@IBOutlet private var installAltStoreMenuItem: NSMenuItem!
|
|
||||||
@IBOutlet private var sideloadAppMenuItem: NSMenuItem!
|
|
||||||
|
|
||||||
private weak var authenticationAppleIDTextField: NSTextField?
|
|
||||||
private weak var authenticationPasswordTextField: NSSecureTextField?
|
|
||||||
|
|
||||||
private var connectedDevicesMenuController: MenuController<ALTDevice>!
|
|
||||||
private var sideloadIPAConnectedDevicesMenuController: MenuController<ALTDevice>!
|
|
||||||
private var enableJITMenuController: MenuController<ALTDevice>!
|
|
||||||
|
|
||||||
private var _jitAppListMenuControllers = [AnyObject]()
|
|
||||||
|
|
||||||
private var isAltPluginUpdateAvailable = false
|
|
||||||
|
|
||||||
private var popoverController: NSPopover?
|
|
||||||
private var popoverError: NSError?
|
|
||||||
private var errorAlert: NSAlert?
|
|
||||||
|
|
||||||
func applicationDidFinishLaunching(_ aNotification: Notification)
|
|
||||||
{
|
|
||||||
UserDefaults.standard.registerDefaults()
|
|
||||||
|
|
||||||
UNUserNotificationCenter.current().delegate = self
|
|
||||||
|
|
||||||
ServerConnectionManager.shared.start()
|
|
||||||
ALTDeviceManager.shared.start()
|
|
||||||
|
|
||||||
#if STAGING
|
|
||||||
SUUpdater.shared().feedURL = URL(string: "https://altstore.io/altserver/sparkle-macos-staging.xml")
|
|
||||||
#else
|
|
||||||
SUUpdater.shared().feedURL = URL(string: "https://altstore.io/altserver/sparkle-macos.xml")
|
|
||||||
#endif
|
|
||||||
|
|
||||||
let item = NSStatusBar.system.statusItem(withLength: -1)
|
|
||||||
item.menu = self.appMenu
|
|
||||||
item.button?.image = NSImage(named: "MenuBarIcon")
|
|
||||||
self.statusItem = item
|
|
||||||
|
|
||||||
self.appMenu.delegate = self
|
|
||||||
|
|
||||||
self.sideloadAppMenuItem.keyEquivalentModifierMask = .option
|
|
||||||
self.sideloadAppMenuItem.isAlternate = true
|
|
||||||
|
|
||||||
let placeholder = NSLocalizedString("No Connected Devices", comment: "")
|
|
||||||
|
|
||||||
self.connectedDevicesMenuController = MenuController<ALTDevice>(menu: self.connectedDevicesMenu, items: [])
|
|
||||||
self.connectedDevicesMenuController.placeholder = placeholder
|
|
||||||
self.connectedDevicesMenuController.action = { [weak self] device in
|
|
||||||
self?.installAltStore(to: device)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.sideloadIPAConnectedDevicesMenuController = MenuController<ALTDevice>(menu: self.sideloadIPAConnectedDevicesMenu, items: [])
|
|
||||||
self.sideloadIPAConnectedDevicesMenuController.placeholder = placeholder
|
|
||||||
self.sideloadIPAConnectedDevicesMenuController.action = { [weak self] device in
|
|
||||||
self?.sideloadIPA(to: device)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.enableJITMenuController = MenuController<ALTDevice>(menu: self.enableJITMenu, items: [])
|
|
||||||
self.enableJITMenuController.placeholder = placeholder
|
|
||||||
|
|
||||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) { (success, error) in
|
|
||||||
guard success else { return }
|
|
||||||
|
|
||||||
if !UserDefaults.standard.didPresentInitialNotification
|
|
||||||
{
|
|
||||||
let content = UNMutableNotificationContent()
|
|
||||||
content.title = NSLocalizedString("AltServer Running", comment: "")
|
|
||||||
content.body = NSLocalizedString("AltServer runs in the background as a menu bar app listening for AltStore.", comment: "")
|
|
||||||
|
|
||||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
|
||||||
UNUserNotificationCenter.current().add(request)
|
|
||||||
|
|
||||||
UserDefaults.standard.didPresentInitialNotification = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.pluginManager.isUpdateAvailable { result in
|
|
||||||
guard let isUpdateAvailable = try? result.get() else { return }
|
|
||||||
self.isAltPluginUpdateAvailable = isUpdateAvailable
|
|
||||||
|
|
||||||
if isUpdateAvailable
|
|
||||||
{
|
|
||||||
self.installMailPlugin()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func applicationWillTerminate(_ aNotification: Notification)
|
|
||||||
{
|
|
||||||
// Insert code here to tear down your application
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension AppDelegate
|
|
||||||
{
|
|
||||||
@objc func installAltStore(to device: ALTDevice)
|
|
||||||
{
|
|
||||||
self.installApplication(at: nil, to: device)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func sideloadIPA(to device: ALTDevice)
|
|
||||||
{
|
|
||||||
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
|
|
||||||
|
|
||||||
let openPanel = NSOpenPanel()
|
|
||||||
openPanel.canChooseDirectories = false
|
|
||||||
openPanel.allowsMultipleSelection = false
|
|
||||||
openPanel.allowedFileTypes = ["ipa"]
|
|
||||||
openPanel.begin { (response) in
|
|
||||||
guard let fileURL = openPanel.url, response == .OK else { return }
|
|
||||||
self.installApplication(at: fileURL, to: device)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func enableJIT(for app: InstalledApp, on device: ALTDevice)
|
|
||||||
{
|
|
||||||
func finish(_ result: Result<Void, Error>)
|
|
||||||
{
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
switch result
|
|
||||||
{
|
|
||||||
case .failure(let error as NSError):
|
|
||||||
let localizedTitle = String(format: NSLocalizedString("JIT could not be enabled for %@.", comment: ""), app.name)
|
|
||||||
self.showErrorAlert(error: error.withLocalizedTitle(localizedTitle))
|
|
||||||
|
|
||||||
case .success:
|
|
||||||
let alert = NSAlert()
|
|
||||||
alert.messageText = String(format: NSLocalizedString("Successfully enabled JIT for %@.", comment: ""), app.name)
|
|
||||||
alert.informativeText = String(format: NSLocalizedString("JIT will remain enabled until you quit the app. You can now disconnect %@ from your computer.", comment: ""), device.name)
|
|
||||||
alert.runModal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ALTDeviceManager.shared.prepare(device) { (result) in
|
|
||||||
switch result
|
|
||||||
{
|
|
||||||
case .failure(let error as NSError): return finish(.failure(error))
|
|
||||||
case .success:
|
|
||||||
ALTDeviceManager.shared.startDebugConnection(to: device) { (connection, error) in
|
|
||||||
guard let connection = connection else {
|
|
||||||
return finish(.failure(error! as NSError))
|
|
||||||
}
|
|
||||||
|
|
||||||
connection.enableUnsignedCodeExecutionForProcess(withName: app.executableName) { (success, error) in
|
|
||||||
guard success else {
|
|
||||||
return finish(.failure(error!))
|
|
||||||
}
|
|
||||||
|
|
||||||
finish(.success(()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func installApplication(at fileURL: URL?, to device: ALTDevice)
|
|
||||||
{
|
|
||||||
let alert = NSAlert()
|
|
||||||
alert.messageText = NSLocalizedString("Please enter your Apple ID and password.", comment: "")
|
|
||||||
alert.informativeText = NSLocalizedString("Your Apple ID and password are not saved and are only sent to Apple for authentication.", comment: "")
|
|
||||||
|
|
||||||
let textFieldSize = NSSize(width: 300, height: 22)
|
|
||||||
|
|
||||||
let appleIDTextField = NSTextField(frame: NSRect(x: 0, y: 0, width: textFieldSize.width, height: textFieldSize.height))
|
|
||||||
appleIDTextField.delegate = self
|
|
||||||
appleIDTextField.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
appleIDTextField.placeholderString = NSLocalizedString("Apple ID", comment: "")
|
|
||||||
alert.window.initialFirstResponder = appleIDTextField
|
|
||||||
self.authenticationAppleIDTextField = appleIDTextField
|
|
||||||
|
|
||||||
let passwordTextField = NSSecureTextField(frame: NSRect(x: 0, y: 0, width: textFieldSize.width, height: textFieldSize.height))
|
|
||||||
passwordTextField.delegate = self
|
|
||||||
passwordTextField.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
passwordTextField.placeholderString = NSLocalizedString("Password", comment: "")
|
|
||||||
self.authenticationPasswordTextField = passwordTextField
|
|
||||||
|
|
||||||
appleIDTextField.nextKeyView = passwordTextField
|
|
||||||
|
|
||||||
let stackView = NSStackView(frame: NSRect(x: 0, y: 0, width: textFieldSize.width, height: textFieldSize.height * 2))
|
|
||||||
stackView.orientation = .vertical
|
|
||||||
stackView.distribution = .equalSpacing
|
|
||||||
stackView.spacing = 0
|
|
||||||
stackView.addArrangedSubview(appleIDTextField)
|
|
||||||
stackView.addArrangedSubview(passwordTextField)
|
|
||||||
alert.accessoryView = stackView
|
|
||||||
|
|
||||||
alert.addButton(withTitle: NSLocalizedString("Install", comment: ""))
|
|
||||||
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
|
|
||||||
|
|
||||||
self.authenticationAlert = alert
|
|
||||||
self.validate()
|
|
||||||
|
|
||||||
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
|
|
||||||
|
|
||||||
let response = alert.runModal()
|
|
||||||
guard response == .alertFirstButtonReturn else { return }
|
|
||||||
|
|
||||||
let username = appleIDTextField.stringValue
|
|
||||||
let password = passwordTextField.stringValue
|
|
||||||
|
|
||||||
func finish(_ result: Result<ALTApplication, Error>)
|
|
||||||
{
|
|
||||||
switch result
|
|
||||||
{
|
|
||||||
case .success(let application):
|
|
||||||
let content = UNMutableNotificationContent()
|
|
||||||
content.title = NSLocalizedString("Installation Succeeded", comment: "")
|
|
||||||
content.body = String(format: NSLocalizedString("%@ was successfully installed on %@.", comment: ""), application.name, device.name)
|
|
||||||
|
|
||||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
|
||||||
UNUserNotificationCenter.current().add(request)
|
|
||||||
|
|
||||||
case .failure(OperationError.cancelled), .failure(ALTAppleAPIError.requiresTwoFactorAuthentication):
|
|
||||||
// Ignore
|
|
||||||
break
|
|
||||||
|
|
||||||
case .failure(let error):
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.showErrorAlert(error: error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func install()
|
|
||||||
{
|
|
||||||
ALTDeviceManager.shared.installApplication(at: fileURL, to: device, appleID: username, password: password, completion: finish(_:))
|
|
||||||
}
|
|
||||||
|
|
||||||
AnisetteDataManager.shared.isXPCAvailable { isAvailable in
|
|
||||||
if isAvailable
|
|
||||||
{
|
|
||||||
// XPC service is available, so we don't need to install/update Mail plug-in.
|
|
||||||
// Users can still manually do so from the AltServer menu.
|
|
||||||
install()
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
self.pluginManager.isUpdateAvailable { result in
|
|
||||||
switch result
|
|
||||||
{
|
|
||||||
case .failure(let error):
|
|
||||||
let error = (error as NSError).withLocalizedTitle(NSLocalizedString("Could not check for Mail plug-in updates.", comment: ""))
|
|
||||||
finish(.failure(error))
|
|
||||||
|
|
||||||
case .success(let isUpdateAvailable):
|
|
||||||
self.isAltPluginUpdateAvailable = isUpdateAvailable
|
|
||||||
|
|
||||||
if !self.pluginManager.isMailPluginInstalled || isUpdateAvailable
|
|
||||||
{
|
|
||||||
self.installMailPlugin { result in
|
|
||||||
switch result
|
|
||||||
{
|
|
||||||
case .failure: break
|
|
||||||
case .success: install()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
install()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func showErrorAlert(error: Error)
|
|
||||||
{
|
|
||||||
self.popoverError = error as NSError
|
|
||||||
|
|
||||||
let nsError = error as NSError
|
|
||||||
|
|
||||||
var messageComponents = [error.localizedDescription]
|
|
||||||
if let recoverySuggestion = nsError.localizedRecoverySuggestion
|
|
||||||
{
|
|
||||||
messageComponents.append(recoverySuggestion)
|
|
||||||
}
|
|
||||||
|
|
||||||
let title = nsError.localizedTitle ?? NSLocalizedString("Operation Failed", comment: "")
|
|
||||||
let message = messageComponents.joined(separator: "\n\n")
|
|
||||||
|
|
||||||
let alert = NSAlert()
|
|
||||||
alert.alertStyle = .critical
|
|
||||||
alert.messageText = title
|
|
||||||
alert.informativeText = message
|
|
||||||
alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
|
|
||||||
alert.addButton(withTitle: NSLocalizedString("View More Details", comment: ""))
|
|
||||||
|
|
||||||
if let viewMoreButton = alert.buttons.last
|
|
||||||
{
|
|
||||||
viewMoreButton.target = self
|
|
||||||
viewMoreButton.action = #selector(AppDelegate.showDetailedErrorDescription)
|
|
||||||
|
|
||||||
self.errorAlert = alert
|
|
||||||
}
|
|
||||||
|
|
||||||
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
|
|
||||||
|
|
||||||
alert.runModal()
|
|
||||||
|
|
||||||
self.popoverController = nil
|
|
||||||
self.errorAlert = nil
|
|
||||||
self.popoverError = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func showDetailedErrorDescription()
|
|
||||||
{
|
|
||||||
guard let errorAlert, let contentView = errorAlert.window.contentView else { return }
|
|
||||||
|
|
||||||
let errorDetailsViewController = NSStoryboard(name: "Main", bundle: .main).instantiateController(withIdentifier: "errorDetailsViewController") as! ErrorDetailsViewController
|
|
||||||
errorDetailsViewController.error = self.popoverError
|
|
||||||
|
|
||||||
let fittingSize = errorDetailsViewController.view.fittingSize
|
|
||||||
errorDetailsViewController.view.frame.size = fittingSize
|
|
||||||
|
|
||||||
let popoverController = NSPopover()
|
|
||||||
popoverController.contentViewController = errorDetailsViewController
|
|
||||||
popoverController.contentSize = fittingSize
|
|
||||||
popoverController.behavior = .transient
|
|
||||||
popoverController.show(relativeTo: contentView.bounds, of: contentView, preferredEdge: .maxX)
|
|
||||||
self.popoverController = popoverController
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func toggleLaunchAtLogin(_ item: NSMenuItem)
|
|
||||||
{
|
|
||||||
LaunchAtLogin.isEnabled.toggle()
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func handleInstallMailPluginMenuItem(_ item: NSMenuItem)
|
|
||||||
{
|
|
||||||
if !self.pluginManager.isMailPluginInstalled || self.isAltPluginUpdateAvailable
|
|
||||||
{
|
|
||||||
self.installMailPlugin()
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
self.uninstallMailPlugin()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func installMailPlugin(completion: ((Result<Void, Error>) -> Void)? = nil)
|
|
||||||
{
|
|
||||||
self.pluginManager.installMailPlugin { (result) in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
switch result
|
|
||||||
{
|
|
||||||
case .failure(PluginError.cancelled): break
|
|
||||||
case .failure(let error):
|
|
||||||
let alert = NSAlert()
|
|
||||||
alert.messageText = NSLocalizedString("Failed to Install Mail Plug-in", comment: "")
|
|
||||||
alert.informativeText = error.localizedDescription
|
|
||||||
alert.runModal()
|
|
||||||
|
|
||||||
case .success:
|
|
||||||
let alert = NSAlert()
|
|
||||||
alert.messageText = NSLocalizedString("Mail Plug-in Installed", comment: "")
|
|
||||||
alert.informativeText = NSLocalizedString("Please restart Mail and enable AltPlugin in Mail's Preferences. Mail must be running when installing or refreshing apps with AltServer.", comment: "")
|
|
||||||
alert.runModal()
|
|
||||||
|
|
||||||
self.isAltPluginUpdateAvailable = false
|
|
||||||
}
|
|
||||||
|
|
||||||
completion?(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func uninstallMailPlugin()
|
|
||||||
{
|
|
||||||
self.pluginManager.uninstallMailPlugin { (result) in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
switch result
|
|
||||||
{
|
|
||||||
case .failure(PluginError.cancelled): break
|
|
||||||
case .failure(let error):
|
|
||||||
let alert = NSAlert()
|
|
||||||
alert.messageText = NSLocalizedString("Failed to Uninstall Mail Plug-in", comment: "")
|
|
||||||
alert.informativeText = error.localizedDescription
|
|
||||||
alert.runModal()
|
|
||||||
|
|
||||||
case .success:
|
|
||||||
let alert = NSAlert()
|
|
||||||
alert.messageText = NSLocalizedString("Mail Plug-in Uninstalled", comment: "")
|
|
||||||
alert.informativeText = NSLocalizedString("Please restart Mail for changes to take effect. You will not be able to use AltServer until the plug-in is reinstalled.", comment: "")
|
|
||||||
alert.runModal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension AppDelegate: NSMenuDelegate
|
|
||||||
{
|
|
||||||
func menuWillOpen(_ menu: NSMenu)
|
|
||||||
{
|
|
||||||
guard menu == self.appMenu else { return }
|
|
||||||
|
|
||||||
// Clear any cached _jitAppListMenuControllers.
|
|
||||||
self._jitAppListMenuControllers.removeAll()
|
|
||||||
|
|
||||||
self.connectedDevices = ALTDeviceManager.shared.availableDevices
|
|
||||||
|
|
||||||
self.connectedDevicesMenuController.items = self.connectedDevices
|
|
||||||
self.sideloadIPAConnectedDevicesMenuController.items = self.connectedDevices
|
|
||||||
self.enableJITMenuController.items = self.connectedDevices
|
|
||||||
|
|
||||||
self.launchAtLoginMenuItem.target = self
|
|
||||||
self.launchAtLoginMenuItem.action = #selector(AppDelegate.toggleLaunchAtLogin(_:))
|
|
||||||
self.launchAtLoginMenuItem.state = LaunchAtLogin.isEnabled ? .on : .off
|
|
||||||
|
|
||||||
if self.isAltPluginUpdateAvailable
|
|
||||||
{
|
|
||||||
self.installMailPluginMenuItem.title = NSLocalizedString("Update Mail Plug-in…", comment: "")
|
|
||||||
}
|
|
||||||
else if self.pluginManager.isMailPluginInstalled
|
|
||||||
{
|
|
||||||
self.installMailPluginMenuItem.title = NSLocalizedString("Uninstall Mail Plug-in…", comment: "")
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
self.installMailPluginMenuItem.title = NSLocalizedString("Install Mail Plug-in…", comment: "")
|
|
||||||
}
|
|
||||||
self.installMailPluginMenuItem.target = self
|
|
||||||
self.installMailPluginMenuItem.action = #selector(AppDelegate.handleInstallMailPluginMenuItem(_:))
|
|
||||||
|
|
||||||
// Need to re-set this every time menu appears so we can refresh device app list.
|
|
||||||
self.enableJITMenuController.submenuHandler = { [weak self] device in
|
|
||||||
let submenu = NSMenu(title: NSLocalizedString("Sideloaded Apps", comment: ""))
|
|
||||||
|
|
||||||
guard let `self` = self else { return submenu }
|
|
||||||
|
|
||||||
let submenuController = MenuController<InstalledApp>(menu: submenu, items: [])
|
|
||||||
submenuController.placeholder = NSLocalizedString("Loading...", comment: "")
|
|
||||||
submenuController.action = { [weak self] (appInfo) in
|
|
||||||
self?.enableJIT(for: appInfo, on: device)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep strong reference
|
|
||||||
self._jitAppListMenuControllers.append(submenuController)
|
|
||||||
|
|
||||||
ALTDeviceManager.shared.fetchInstalledApps(on: device) { (installedApps, error) in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
guard let installedApps = installedApps else {
|
|
||||||
print("Failed to fetch installed apps from \(device).", error!)
|
|
||||||
submenuController.placeholder = error?.localizedDescription
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
print("Fetched \(installedApps.count) apps for \(device).")
|
|
||||||
|
|
||||||
let sortedApps = installedApps.sorted { (app1, app2) in
|
|
||||||
if app1.name == app2.name
|
|
||||||
{
|
|
||||||
return app1.bundleIdentifier < app2.bundleIdentifier
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return app1.name < app2.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
submenuController.items = sortedApps
|
|
||||||
|
|
||||||
if submenuController.items.isEmpty
|
|
||||||
{
|
|
||||||
submenuController.placeholder = NSLocalizedString("No Sideloaded Apps", comment: "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return submenu
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func menuDidClose(_ menu: NSMenu)
|
|
||||||
{
|
|
||||||
guard menu == self.appMenu else { return }
|
|
||||||
|
|
||||||
// Clearing _jitAppListMenuControllers now prevents action handler from being called.
|
|
||||||
// self._jitAppListMenuControllers = []
|
|
||||||
|
|
||||||
// Set `submenuHandler` to nil to prevent prematurely fetching installed apps in menuWillOpen(_:)
|
|
||||||
// when assigning self.connectedDevices to `items` (which implicitly calls `submenuHandler`)
|
|
||||||
self.enableJITMenuController.submenuHandler = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func menu(_ menu: NSMenu, willHighlight item: NSMenuItem?)
|
|
||||||
{
|
|
||||||
guard menu == self.appMenu else { return }
|
|
||||||
|
|
||||||
// The submenu won't update correctly if the user holds/releases
|
|
||||||
// the Option key while the submenu is visible.
|
|
||||||
// Workaround: temporarily set submenu to nil to dismiss it,
|
|
||||||
// which will then cause the correct submenu to appear.
|
|
||||||
|
|
||||||
let previousItem: NSMenuItem
|
|
||||||
switch item
|
|
||||||
{
|
|
||||||
case self.sideloadAppMenuItem: previousItem = self.installAltStoreMenuItem
|
|
||||||
case self.installAltStoreMenuItem: previousItem = self.sideloadAppMenuItem
|
|
||||||
default: return
|
|
||||||
}
|
|
||||||
|
|
||||||
let submenu = previousItem.submenu
|
|
||||||
previousItem.submenu = nil
|
|
||||||
previousItem.submenu = submenu
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension AppDelegate: NSTextFieldDelegate
|
|
||||||
{
|
|
||||||
func controlTextDidChange(_ obj: Notification)
|
|
||||||
{
|
|
||||||
self.validate()
|
|
||||||
}
|
|
||||||
|
|
||||||
func controlTextDidEndEditing(_ obj: Notification)
|
|
||||||
{
|
|
||||||
self.validate()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func validate()
|
|
||||||
{
|
|
||||||
guard
|
|
||||||
let appleID = self.authenticationAppleIDTextField?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
||||||
let password = self.authenticationPasswordTextField?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
else { return }
|
|
||||||
|
|
||||||
if appleID.isEmpty || password.isEmpty
|
|
||||||
{
|
|
||||||
self.authenticationAlert?.buttons.first?.isEnabled = false
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
self.authenticationAlert?.buttons.first?.isEnabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
self.authenticationAlert?.layout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension AppDelegate: UNUserNotificationCenterDelegate
|
|
||||||
{
|
|
||||||
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void)
|
|
||||||
{
|
|
||||||
completionHandler([.alert, .sound, .badge])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"size" : "16x16",
|
|
||||||
"idiom" : "mac",
|
|
||||||
"filename" : "Icon@16.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "16x16",
|
|
||||||
"idiom" : "mac",
|
|
||||||
"filename" : "Icon@32-1.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "32x32",
|
|
||||||
"idiom" : "mac",
|
|
||||||
"filename" : "Icon@32.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "32x32",
|
|
||||||
"idiom" : "mac",
|
|
||||||
"filename" : "Icon@64.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "128x128",
|
|
||||||
"idiom" : "mac",
|
|
||||||
"filename" : "Icon@128.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "128x128",
|
|
||||||
"idiom" : "mac",
|
|
||||||
"filename" : "Icon@256-1.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "256x256",
|
|
||||||
"idiom" : "mac",
|
|
||||||
"filename" : "Icon@256.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "256x256",
|
|
||||||
"idiom" : "mac",
|
|
||||||
"filename" : "Icon@512-1.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "512x512",
|
|
||||||
"idiom" : "mac",
|
|
||||||
"filename" : "Icon@512.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "512x512",
|
|
||||||
"idiom" : "mac",
|
|
||||||
"filename" : "Icon@1024.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"version" : 1,
|
|
||||||
"author" : "xcode"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 294 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"info" : {
|
|
||||||
"version" : 1,
|
|
||||||
"author" : "xcode"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"filename" : "MenuBar@19.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"filename" : "MenuBar@38.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"version" : 1,
|
|
||||||
"author" : "xcode"
|
|
||||||
},
|
|
||||||
"properties" : {
|
|
||||||
"template-rendering-intent" : "template"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
@@ -1,468 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
|
||||||
<dependencies>
|
|
||||||
<deployment identifier="macosx"/>
|
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/>
|
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
|
||||||
</dependencies>
|
|
||||||
<scenes>
|
|
||||||
<!--Application-->
|
|
||||||
<scene sceneID="JPo-4y-FX3">
|
|
||||||
<objects>
|
|
||||||
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
|
||||||
<stackView distribution="fill" orientation="vertical" alignment="leading" spacing="4" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" id="urc-xw-Dhc">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="300" height="46"/>
|
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
|
||||||
<subviews>
|
|
||||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="zLd-d8-ghZ">
|
|
||||||
<rect key="frame" x="0.0" y="25" width="300" height="21"/>
|
|
||||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Apple ID" drawsBackground="YES" id="BXa-Re-rs3">
|
|
||||||
<font key="font" metaFont="system"/>
|
|
||||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
|
||||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
|
||||||
</textFieldCell>
|
|
||||||
<connections>
|
|
||||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="QtW-r2-Vuh"/>
|
|
||||||
<outlet property="nextKeyView" destination="9rp-Vx-rvB" id="bQY-qj-Sej"/>
|
|
||||||
</connections>
|
|
||||||
</textField>
|
|
||||||
<secureTextField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="9rp-Vx-rvB">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="300" height="21"/>
|
|
||||||
<secureTextFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Password" drawsBackground="YES" usesSingleLineMode="YES" id="xqJ-wt-DlP">
|
|
||||||
<font key="font" metaFont="system"/>
|
|
||||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
|
||||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
|
||||||
<allowedInputSourceLocales>
|
|
||||||
<string>NSAllRomanInputSourcesLocaleIdentifier</string>
|
|
||||||
</allowedInputSourceLocales>
|
|
||||||
</secureTextFieldCell>
|
|
||||||
<connections>
|
|
||||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="qav-xj-izy"/>
|
|
||||||
</connections>
|
|
||||||
</secureTextField>
|
|
||||||
</subviews>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstItem="9rp-Vx-rvB" firstAttribute="width" secondItem="urc-xw-Dhc" secondAttribute="width" id="Eht-pU-Gyh"/>
|
|
||||||
<constraint firstItem="zLd-d8-ghZ" firstAttribute="width" secondItem="urc-xw-Dhc" secondAttribute="width" id="mg7-Kq-abL"/>
|
|
||||||
<constraint firstAttribute="width" constant="300" id="zqf-x6-BET"/>
|
|
||||||
</constraints>
|
|
||||||
<visibilityPriorities>
|
|
||||||
<integer value="1000"/>
|
|
||||||
<integer value="1000"/>
|
|
||||||
</visibilityPriorities>
|
|
||||||
<customSpacing>
|
|
||||||
<real value="3.4028234663852886e+38"/>
|
|
||||||
<real value="3.4028234663852886e+38"/>
|
|
||||||
</customSpacing>
|
|
||||||
</stackView>
|
|
||||||
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
|
||||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="AltServer" customModuleProvider="target">
|
|
||||||
<connections>
|
|
||||||
<outlet property="appMenu" destination="uQy-DD-JDr" id="7cY-Ov-AOW"/>
|
|
||||||
<outlet property="authenticationAppleIDTextField" destination="zLd-d8-ghZ" id="wW5-0J-zdq"/>
|
|
||||||
<outlet property="authenticationPasswordTextField" destination="9rp-Vx-rvB" id="ZoC-DI-jzQ"/>
|
|
||||||
<outlet property="connectedDevicesMenu" destination="KJ9-WY-pW1" id="Mcv-64-iFU"/>
|
|
||||||
<outlet property="enableJITMenu" destination="la4-Sa-L3C" id="YrW-hR-uA7"/>
|
|
||||||
<outlet property="installAltStoreMenuItem" destination="MJ8-Lt-SSV" id="KYp-c5-8Ru"/>
|
|
||||||
<outlet property="installMailPluginMenuItem" destination="3CM-gV-X2G" id="lio-ha-z0S"/>
|
|
||||||
<outlet property="launchAtLoginMenuItem" destination="IyR-FQ-upe" id="Fxn-EP-hwH"/>
|
|
||||||
<outlet property="sideloadAppMenuItem" destination="x0e-zI-0A2" id="GJo-FY-1GO"/>
|
|
||||||
<outlet property="sideloadIPAConnectedDevicesMenu" destination="IuI-bV-fTY" id="QQw-St-HfG"/>
|
|
||||||
</connections>
|
|
||||||
</customObject>
|
|
||||||
<customObject id="Arf-IC-5eb" customClass="SUUpdater"/>
|
|
||||||
<application id="hnw-xV-0zn" sceneMemberID="viewController">
|
|
||||||
<menu key="mainMenu" title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
|
|
||||||
<items>
|
|
||||||
<menuItem title="AltServer" id="1Xt-HY-uBw">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<menu key="submenu" title="AltServer" systemMenu="apple" id="uQy-DD-JDr">
|
|
||||||
<items>
|
|
||||||
<menuItem title="About AltServer" id="5kV-Vb-QxS">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<connections>
|
|
||||||
<action selector="orderFrontStandardAboutPanel:" target="Ady-hI-5gd" id="Exp-CZ-Vem"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
|
|
||||||
<menuItem title="Install AltStore…" id="MJ8-Lt-SSV">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<menu key="submenu" title="Install AltStore…" systemMenu="recentDocuments" id="KJ9-WY-pW1">
|
|
||||||
<items>
|
|
||||||
<menuItem title="No Connected Devices" id="N5N-3K-XuR">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<connections>
|
|
||||||
<action selector="clearRecentDocuments:" target="Ady-hI-5gd" id="DKG-yI-Ujv"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
</items>
|
|
||||||
</menu>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Sideload .ipa…" id="x0e-zI-0A2" userLabel="Install .ipa">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<menu key="submenu" title="Sideload .ipa…" systemMenu="recentDocuments" id="IuI-bV-fTY">
|
|
||||||
<items>
|
|
||||||
<menuItem title="No Connected Devices" id="in5-an-MD0">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<connections>
|
|
||||||
<action selector="clearRecentDocuments:" target="Ady-hI-5gd" id="aUE-On-axK"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
</items>
|
|
||||||
</menu>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Enable JIT" id="TvL-8M-oPZ">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<menu key="submenu" title="Enable JIT" systemMenu="recentDocuments" id="la4-Sa-L3C">
|
|
||||||
<items>
|
|
||||||
<menuItem title="No Connected Devices" id="Aya-FP-EzE">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
</menuItem>
|
|
||||||
</items>
|
|
||||||
</menu>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem isSeparatorItem="YES" id="1ZZ-BB-xHy"/>
|
|
||||||
<menuItem title="Launch at Login" id="IyR-FQ-upe" userLabel="Launch At Login">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Install Mail Plug-in…" id="3CM-gV-X2G" userLabel="Mail Plug-in">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem isSeparatorItem="YES" id="mVM-Nm-Zi9"/>
|
|
||||||
<menuItem title="Check for Updates..." id="Tnq-gD-Eic" userLabel="Check for Updates">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<connections>
|
|
||||||
<action selector="checkForUpdates:" target="Arf-IC-5eb" id="7JG-du-nr4"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem isSeparatorItem="YES" id="hmG-xg-qgm"/>
|
|
||||||
<menuItem title="Quit AltServer" keyEquivalent="q" id="4sb-4s-VLi">
|
|
||||||
<connections>
|
|
||||||
<action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
</items>
|
|
||||||
</menu>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Edit" id="5QF-Oa-p0T">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
|
|
||||||
<items>
|
|
||||||
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
|
|
||||||
<connections>
|
|
||||||
<action selector="undo:" target="Ady-hI-5gd" id="M6e-cu-g7V"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
|
|
||||||
<connections>
|
|
||||||
<action selector="redo:" target="Ady-hI-5gd" id="oIA-Rs-6OD"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
|
|
||||||
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
|
|
||||||
<connections>
|
|
||||||
<action selector="cut:" target="Ady-hI-5gd" id="YJe-68-I9s"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
|
|
||||||
<connections>
|
|
||||||
<action selector="copy:" target="Ady-hI-5gd" id="G1f-GL-Joy"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
|
|
||||||
<connections>
|
|
||||||
<action selector="paste:" target="Ady-hI-5gd" id="UvS-8e-Qdg"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
|
||||||
<connections>
|
|
||||||
<action selector="pasteAsPlainText:" target="Ady-hI-5gd" id="cEh-KX-wJQ"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Delete" id="pa3-QI-u2k">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<connections>
|
|
||||||
<action selector="delete:" target="Ady-hI-5gd" id="0Mk-Ml-PaM"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
|
|
||||||
<connections>
|
|
||||||
<action selector="selectAll:" target="Ady-hI-5gd" id="VNm-Mi-diN"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
|
|
||||||
<menuItem title="Find" id="4EN-yA-p0u">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<menu key="submenu" title="Find" id="1b7-l0-nxx">
|
|
||||||
<items>
|
|
||||||
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
|
|
||||||
<connections>
|
|
||||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="cD7-Qs-BN4"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
|
||||||
<connections>
|
|
||||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="WD3-Gg-5AJ"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
|
|
||||||
<connections>
|
|
||||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="NDo-RZ-v9R"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
|
|
||||||
<connections>
|
|
||||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="HOh-sY-3ay"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
|
|
||||||
<connections>
|
|
||||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="U76-nv-p5D"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
|
|
||||||
<connections>
|
|
||||||
<action selector="centerSelectionInVisibleArea:" target="Ady-hI-5gd" id="IOG-6D-g5B"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
</items>
|
|
||||||
</menu>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
|
|
||||||
<items>
|
|
||||||
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
|
|
||||||
<connections>
|
|
||||||
<action selector="showGuessPanel:" target="Ady-hI-5gd" id="vFj-Ks-hy3"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
|
|
||||||
<connections>
|
|
||||||
<action selector="checkSpelling:" target="Ady-hI-5gd" id="fz7-VC-reM"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
|
|
||||||
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<connections>
|
|
||||||
<action selector="toggleContinuousSpellChecking:" target="Ady-hI-5gd" id="7w6-Qz-0kB"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<connections>
|
|
||||||
<action selector="toggleGrammarChecking:" target="Ady-hI-5gd" id="muD-Qn-j4w"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<connections>
|
|
||||||
<action selector="toggleAutomaticSpellingCorrection:" target="Ady-hI-5gd" id="2lM-Qi-WAP"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
</items>
|
|
||||||
</menu>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Substitutions" id="9ic-FL-obx">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
|
|
||||||
<items>
|
|
||||||
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<connections>
|
|
||||||
<action selector="orderFrontSubstitutionsPanel:" target="Ady-hI-5gd" id="oku-mr-iSq"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
|
|
||||||
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<connections>
|
|
||||||
<action selector="toggleSmartInsertDelete:" target="Ady-hI-5gd" id="3IJ-Se-DZD"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<connections>
|
|
||||||
<action selector="toggleAutomaticQuoteSubstitution:" target="Ady-hI-5gd" id="ptq-xd-QOA"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<connections>
|
|
||||||
<action selector="toggleAutomaticDashSubstitution:" target="Ady-hI-5gd" id="oCt-pO-9gS"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Smart Links" id="cwL-P1-jid">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<connections>
|
|
||||||
<action selector="toggleAutomaticLinkDetection:" target="Ady-hI-5gd" id="Gip-E3-Fov"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Data Detectors" id="tRr-pd-1PS">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<connections>
|
|
||||||
<action selector="toggleAutomaticDataDetection:" target="Ady-hI-5gd" id="R1I-Nq-Kbl"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<connections>
|
|
||||||
<action selector="toggleAutomaticTextReplacement:" target="Ady-hI-5gd" id="DvP-Fe-Py6"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
</items>
|
|
||||||
</menu>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Transformations" id="2oI-Rn-ZJC">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
|
|
||||||
<items>
|
|
||||||
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<connections>
|
|
||||||
<action selector="uppercaseWord:" target="Ady-hI-5gd" id="sPh-Tk-edu"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<connections>
|
|
||||||
<action selector="lowercaseWord:" target="Ady-hI-5gd" id="iUZ-b5-hil"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<connections>
|
|
||||||
<action selector="capitalizeWord:" target="Ady-hI-5gd" id="26H-TL-nsh"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
</items>
|
|
||||||
</menu>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Speech" id="xrE-MZ-jX0">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
|
|
||||||
<items>
|
|
||||||
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<connections>
|
|
||||||
<action selector="startSpeaking:" target="Ady-hI-5gd" id="654-Ng-kyl"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<connections>
|
|
||||||
<action selector="stopSpeaking:" target="Ady-hI-5gd" id="dX8-6p-jy9"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
</items>
|
|
||||||
</menu>
|
|
||||||
</menuItem>
|
|
||||||
</items>
|
|
||||||
</menu>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem title="Help" id="wpr-3q-Mcd">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
|
|
||||||
<items>
|
|
||||||
<menuItem title="AltServer Help" keyEquivalent="?" id="FKE-Sm-Kum">
|
|
||||||
<connections>
|
|
||||||
<action selector="showHelp:" target="Ady-hI-5gd" id="y7X-2Q-9no"/>
|
|
||||||
</connections>
|
|
||||||
</menuItem>
|
|
||||||
</items>
|
|
||||||
</menu>
|
|
||||||
</menuItem>
|
|
||||||
</items>
|
|
||||||
</menu>
|
|
||||||
<connections>
|
|
||||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
|
|
||||||
</connections>
|
|
||||||
</application>
|
|
||||||
</objects>
|
|
||||||
<point key="canvasLocation" x="75" y="0.0"/>
|
|
||||||
</scene>
|
|
||||||
<!--Error Details View Controller-->
|
|
||||||
<scene sceneID="IpC-sd-lPu">
|
|
||||||
<objects>
|
|
||||||
<viewController storyboardIdentifier="errorDetailsViewController" id="vPa-1q-slD" customClass="ErrorDetailsViewController" customModule="AltServer" customModuleProvider="target" sceneMemberID="viewController">
|
|
||||||
<view key="view" id="umL-zC-zP8">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="450" height="124"/>
|
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
|
||||||
<subviews>
|
|
||||||
<stackView distribution="fill" orientation="vertical" alignment="leading" spacing="16" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="aUr-m2-nXm">
|
|
||||||
<rect key="frame" x="20" y="20" width="410" height="84"/>
|
|
||||||
<subviews>
|
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="8GZ-nV-XXA">
|
|
||||||
<rect key="frame" x="-2" y="68" width="40" height="16"/>
|
|
||||||
<textFieldCell key="cell" lineBreakMode="clipping" selectable="YES" title="Label" id="V5D-v5-MVX">
|
|
||||||
<font key="font" metaFont="systemBold"/>
|
|
||||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
|
||||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
|
||||||
</textFieldCell>
|
|
||||||
</textField>
|
|
||||||
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="j2o-2b-63k">
|
|
||||||
<rect key="frame" x="-2" y="36" width="92" height="16"/>
|
|
||||||
<textFieldCell key="cell" selectable="YES" title="Multiline Label" id="3jf-Z7-88l">
|
|
||||||
<font key="font" metaFont="system"/>
|
|
||||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
|
||||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
|
||||||
</textFieldCell>
|
|
||||||
</textField>
|
|
||||||
<stackView distribution="equalSpacing" orientation="vertical" alignment="trailing" spacing="0.0" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="XOa-gj-Rz2">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="410" height="20"/>
|
|
||||||
<subviews>
|
|
||||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="iS8-eX-YvA">
|
|
||||||
<rect key="frame" x="312" y="-7" width="105" height="32"/>
|
|
||||||
<buttonCell key="cell" type="push" title="Search FAQ" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="Fpi-GA-lVc">
|
|
||||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
|
||||||
<font key="font" metaFont="system"/>
|
|
||||||
</buttonCell>
|
|
||||||
<connections>
|
|
||||||
<action selector="searchFAQ:" target="vPa-1q-slD" id="WIx-Uo-GVu"/>
|
|
||||||
</connections>
|
|
||||||
</button>
|
|
||||||
</subviews>
|
|
||||||
<visibilityPriorities>
|
|
||||||
<integer value="1000"/>
|
|
||||||
</visibilityPriorities>
|
|
||||||
<customSpacing>
|
|
||||||
<real value="3.4028234663852886e+38"/>
|
|
||||||
</customSpacing>
|
|
||||||
</stackView>
|
|
||||||
</subviews>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstItem="XOa-gj-Rz2" firstAttribute="width" secondItem="aUr-m2-nXm" secondAttribute="width" id="AGA-99-d6r"/>
|
|
||||||
</constraints>
|
|
||||||
<visibilityPriorities>
|
|
||||||
<integer value="1000"/>
|
|
||||||
<integer value="1000"/>
|
|
||||||
<integer value="1000"/>
|
|
||||||
</visibilityPriorities>
|
|
||||||
<customSpacing>
|
|
||||||
<real value="3.4028234663852886e+38"/>
|
|
||||||
<real value="3.4028234663852886e+38"/>
|
|
||||||
<real value="3.4028234663852886e+38"/>
|
|
||||||
</customSpacing>
|
|
||||||
</stackView>
|
|
||||||
</subviews>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="trailing" secondItem="aUr-m2-nXm" secondAttribute="trailing" constant="20" id="RnW-JC-q92"/>
|
|
||||||
<constraint firstAttribute="bottom" secondItem="aUr-m2-nXm" secondAttribute="bottom" constant="20" id="eGe-BS-TFJ"/>
|
|
||||||
<constraint firstItem="aUr-m2-nXm" firstAttribute="top" secondItem="umL-zC-zP8" secondAttribute="top" constant="20" id="fel-H8-WFO"/>
|
|
||||||
<constraint firstItem="aUr-m2-nXm" firstAttribute="leading" secondItem="umL-zC-zP8" secondAttribute="leading" constant="20" id="tcH-p8-4QH"/>
|
|
||||||
</constraints>
|
|
||||||
</view>
|
|
||||||
<connections>
|
|
||||||
<outlet property="detailedDescriptionLabel" destination="j2o-2b-63k" id="de3-yf-oTt"/>
|
|
||||||
<outlet property="errorCodeLabel" destination="8GZ-nV-XXA" id="cXZ-11-wrJ"/>
|
|
||||||
</connections>
|
|
||||||
</viewController>
|
|
||||||
<customObject id="wrv-m0-8zg" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
|
||||||
</objects>
|
|
||||||
<point key="canvasLocation" x="537" y="17"/>
|
|
||||||
</scene>
|
|
||||||
</scenes>
|
|
||||||
</document>
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
//
|
|
||||||
// NSError+libimobiledevice.h
|
|
||||||
// AltServer
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 3/23/21.
|
|
||||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <libimobiledevice/mobile_image_mounter.h>
|
|
||||||
#import <libimobiledevice/debugserver.h>
|
|
||||||
#import <libimobiledevice/installation_proxy.h>
|
|
||||||
|
|
||||||
#import <AltSign/ALTDevice.h>
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
|
||||||
|
|
||||||
@interface NSError (libimobiledevice)
|
|
||||||
|
|
||||||
+ (nullable instancetype)errorWithMobileImageMounterError:(mobile_image_mounter_error_t)error device:(nullable ALTDevice *)device;
|
|
||||||
+ (nullable instancetype)errorWithDebugServerError:(debugserver_error_t)error device:(nullable ALTDevice *)device;
|
|
||||||
+ (nullable instancetype)errorWithInstallationProxyError:(instproxy_error_t)error device:(nullable ALTDevice *)device;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
//
|
|
||||||
// NSError+libimobiledevice.m
|
|
||||||
// AltServer
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 3/23/21.
|
|
||||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "NSError+libimobiledevice.h"
|
|
||||||
#import "NSError+ALTServerError.h"
|
|
||||||
|
|
||||||
@implementation NSError (libimobiledevice)
|
|
||||||
|
|
||||||
+ (nullable instancetype)errorWithMobileImageMounterError:(mobile_image_mounter_error_t)error device:(nullable ALTDevice *)device
|
|
||||||
{
|
|
||||||
NSMutableDictionary *userInfo = [@{
|
|
||||||
ALTUnderlyingErrorDomainErrorKey: @"Mobile Image Mounter",
|
|
||||||
ALTUnderlyingErrorCodeErrorKey: [@(error) description],
|
|
||||||
} mutableCopy];
|
|
||||||
|
|
||||||
if (device)
|
|
||||||
{
|
|
||||||
userInfo[ALTDeviceNameErrorKey] = device.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (error)
|
|
||||||
{
|
|
||||||
case MOBILE_IMAGE_MOUNTER_E_SUCCESS: return nil;
|
|
||||||
case MOBILE_IMAGE_MOUNTER_E_INVALID_ARG: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorInvalidRequest userInfo:userInfo];
|
|
||||||
case MOBILE_IMAGE_MOUNTER_E_PLIST_ERROR: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorInvalidResponse userInfo:userInfo];
|
|
||||||
case MOBILE_IMAGE_MOUNTER_E_CONN_FAILED: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorUsbmuxd userInfo:userInfo];
|
|
||||||
case MOBILE_IMAGE_MOUNTER_E_COMMAND_FAILED: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorInvalidRequest userInfo:userInfo];
|
|
||||||
case MOBILE_IMAGE_MOUNTER_E_DEVICE_LOCKED: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorDeviceLocked userInfo:userInfo];
|
|
||||||
case MOBILE_IMAGE_MOUNTER_E_UNKNOWN_ERROR: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorUnknown userInfo:userInfo];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
+ (nullable instancetype)errorWithDebugServerError:(debugserver_error_t)error device:(nullable ALTDevice *)device
|
|
||||||
{
|
|
||||||
NSMutableDictionary *userInfo = [@{
|
|
||||||
ALTUnderlyingErrorDomainErrorKey: @"Debug Server",
|
|
||||||
ALTUnderlyingErrorCodeErrorKey: [@(error) description],
|
|
||||||
} mutableCopy];
|
|
||||||
|
|
||||||
if (device)
|
|
||||||
{
|
|
||||||
userInfo[ALTDeviceNameErrorKey] = device.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (error)
|
|
||||||
{
|
|
||||||
case DEBUGSERVER_E_SUCCESS: return nil;
|
|
||||||
case DEBUGSERVER_E_INVALID_ARG: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorInvalidRequest userInfo:userInfo];
|
|
||||||
case DEBUGSERVER_E_MUX_ERROR: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorUsbmuxd userInfo:userInfo];
|
|
||||||
case DEBUGSERVER_E_SSL_ERROR: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorSSL userInfo:userInfo];
|
|
||||||
case DEBUGSERVER_E_RESPONSE_ERROR: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorInvalidResponse userInfo:userInfo];
|
|
||||||
case DEBUGSERVER_E_TIMEOUT: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorTimedOut userInfo:userInfo];
|
|
||||||
case DEBUGSERVER_E_UNKNOWN_ERROR: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorUnknown userInfo:userInfo];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
+ (nullable instancetype)errorWithInstallationProxyError:(instproxy_error_t)error device:(nullable ALTDevice *)device
|
|
||||||
{
|
|
||||||
NSMutableDictionary *userInfo = [@{
|
|
||||||
ALTUnderlyingErrorDomainErrorKey: @"Installation Proxy",
|
|
||||||
ALTUnderlyingErrorCodeErrorKey: [@(error) description],
|
|
||||||
} mutableCopy];
|
|
||||||
|
|
||||||
if (device)
|
|
||||||
{
|
|
||||||
userInfo[ALTDeviceNameErrorKey] = device.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (error)
|
|
||||||
{
|
|
||||||
case INSTPROXY_E_SUCCESS: return nil;
|
|
||||||
case INSTPROXY_E_INVALID_ARG: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorInvalidRequest userInfo:userInfo];
|
|
||||||
case INSTPROXY_E_PLIST_ERROR: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorInvalidResponse userInfo:userInfo];
|
|
||||||
case INSTPROXY_E_CONN_FAILED: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorUsbmuxd userInfo:userInfo];
|
|
||||||
case INSTPROXY_E_RECEIVE_TIMEOUT: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorTimedOut userInfo:userInfo];
|
|
||||||
// case INSTPROXY_E_DEVICE_OS_VERSION_TOO_LOW: return [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorUnsupportediOSVersion userInfo:nil]; // Error message assumes we're installing AltStore
|
|
||||||
default: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorUnknown userInfo:userInfo];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
//
|
|
||||||
// ALTDebugConnection+Private.h
|
|
||||||
// AltServer
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 2/19/21.
|
|
||||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "ALTDebugConnection.h"
|
|
||||||
|
|
||||||
#include <libimobiledevice/libimobiledevice.h>
|
|
||||||
#include <libimobiledevice/debugserver.h>
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
|
||||||
|
|
||||||
@interface ALTDebugConnection ()
|
|
||||||
|
|
||||||
@property (nonatomic, readonly) dispatch_queue_t connectionQueue;
|
|
||||||
|
|
||||||
@property (nonatomic, nullable) debugserver_client_t client;
|
|
||||||
|
|
||||||
- (instancetype)initWithDevice:(ALTDevice *)device;
|
|
||||||
|
|
||||||
- (void)connectWithCompletionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
//
|
|
||||||
// ALTDebugConnection.h
|
|
||||||
// AltServer
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 2/19/21.
|
|
||||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "AltSign.h"
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
|
||||||
|
|
||||||
NS_SWIFT_NAME(DebugConnection)
|
|
||||||
@interface ALTDebugConnection : NSObject
|
|
||||||
|
|
||||||
@property (nonatomic, copy, readonly) ALTDevice *device;
|
|
||||||
|
|
||||||
- (void)enableUnsignedCodeExecutionForProcessWithName:(NSString *)processName completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
|
||||||
- (void)enableUnsignedCodeExecutionForProcessWithID:(NSInteger)pid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
|
||||||
|
|
||||||
- (void)disconnect;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
|
||||||
@@ -1,318 +0,0 @@
|
|||||||
//
|
|
||||||
// ALTDebugConnection.m
|
|
||||||
// AltServer
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 2/19/21.
|
|
||||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "ALTDebugConnection+Private.h"
|
|
||||||
|
|
||||||
#import "NSError+ALTServerError.h"
|
|
||||||
#import "NSError+libimobiledevice.h"
|
|
||||||
|
|
||||||
char *bin2hex(const unsigned char *bin, size_t length)
|
|
||||||
{
|
|
||||||
if (bin == NULL || length == 0)
|
|
||||||
{
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
char *hex = (char *)malloc(length * 2 + 1);
|
|
||||||
for (size_t i = 0; i < length; i++)
|
|
||||||
{
|
|
||||||
hex[i * 2] = "0123456789ABCDEF"[bin[i] >> 4];
|
|
||||||
hex[i * 2 + 1] = "0123456789ABCDEF"[bin[i] & 0x0F];
|
|
||||||
}
|
|
||||||
hex[length * 2] = '\0';
|
|
||||||
|
|
||||||
return hex;
|
|
||||||
}
|
|
||||||
|
|
||||||
@implementation ALTDebugConnection
|
|
||||||
|
|
||||||
- (instancetype)initWithDevice:(ALTDevice *)device
|
|
||||||
{
|
|
||||||
self = [super init];
|
|
||||||
if (self)
|
|
||||||
{
|
|
||||||
_device = device;
|
|
||||||
_connectionQueue = dispatch_queue_create_with_target("io.altstore.AltServer.DebugConnection",
|
|
||||||
DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL,
|
|
||||||
dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)dealloc
|
|
||||||
{
|
|
||||||
[self disconnect];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)disconnect
|
|
||||||
{
|
|
||||||
if (_client == nil)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
debugserver_client_free(_client);
|
|
||||||
_client = nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)connectWithCompletionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler
|
|
||||||
{
|
|
||||||
__block idevice_t device = NULL;
|
|
||||||
|
|
||||||
void (^finish)(BOOL, NSError *) = ^(BOOL success, NSError *error) {
|
|
||||||
if (device)
|
|
||||||
{
|
|
||||||
idevice_free(device);
|
|
||||||
}
|
|
||||||
|
|
||||||
completionHandler(success, error);
|
|
||||||
};
|
|
||||||
|
|
||||||
dispatch_async(self.connectionQueue, ^{
|
|
||||||
/* Find Device */
|
|
||||||
if (idevice_new_with_options(&device, self.device.identifier.UTF8String, (enum idevice_options)((int)IDEVICE_LOOKUP_NETWORK | (int)IDEVICE_LOOKUP_USBMUX)) != IDEVICE_E_SUCCESS)
|
|
||||||
{
|
|
||||||
return finish(NO, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorDeviceNotFound userInfo:nil]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Connect to debugserver */
|
|
||||||
debugserver_client_t client = NULL;
|
|
||||||
debugserver_error_t error = debugserver_client_start_service(device, &client, "AltServer");
|
|
||||||
if (error != DEBUGSERVER_E_SUCCESS)
|
|
||||||
{
|
|
||||||
return finish(NO, [NSError errorWithDebugServerError:error device:self.device]);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.client = client;
|
|
||||||
|
|
||||||
finish(YES, nil);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)enableUnsignedCodeExecutionForProcessWithName:(NSString *)processName completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler
|
|
||||||
{
|
|
||||||
[self _enableUnsignedCodeExecutionForProcessWithName:processName pid:0 completionHandler:completionHandler];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)enableUnsignedCodeExecutionForProcessWithID:(NSInteger)pid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler
|
|
||||||
{
|
|
||||||
[self _enableUnsignedCodeExecutionForProcessWithName:nil pid:(int32_t)pid completionHandler:completionHandler];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)_enableUnsignedCodeExecutionForProcessWithName:(nullable NSString *)processName pid:(int32_t)pid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler
|
|
||||||
{
|
|
||||||
dispatch_async(self.connectionQueue, ^{
|
|
||||||
NSString *attachCommand = nil;
|
|
||||||
|
|
||||||
if (processName)
|
|
||||||
{
|
|
||||||
NSString *encodedName = @(bin2hex((const unsigned char *)processName.UTF8String, (size_t)strlen(processName.UTF8String)));
|
|
||||||
attachCommand = [NSString stringWithFormat:@"vAttachOrWait;%@", encodedName];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Convert to Big-endian.
|
|
||||||
int32_t rawPID = CFSwapInt32HostToBig(pid);
|
|
||||||
|
|
||||||
NSString *encodedName = @(bin2hex((const unsigned char *)&rawPID, 4));
|
|
||||||
attachCommand = [NSString stringWithFormat:@"vAttach;%@", encodedName];
|
|
||||||
}
|
|
||||||
|
|
||||||
NSError *error = nil;
|
|
||||||
if (![self sendCommand:attachCommand arguments:nil error:&error])
|
|
||||||
{
|
|
||||||
NSMutableDictionary *userInfo = [error.userInfo mutableCopy];
|
|
||||||
userInfo[ALTAppNameErrorKey] = processName;
|
|
||||||
userInfo[ALTDeviceNameErrorKey] = self.device.name;
|
|
||||||
|
|
||||||
NSError *returnError = [NSError errorWithDomain:error.domain code:error.code userInfo:userInfo];
|
|
||||||
return completionHandler(NO, returnError);
|
|
||||||
}
|
|
||||||
|
|
||||||
NSString *detachCommand = @"D";
|
|
||||||
if (![self sendCommand:detachCommand arguments:nil error:&error])
|
|
||||||
{
|
|
||||||
NSMutableDictionary *userInfo = [error.userInfo mutableCopy];
|
|
||||||
userInfo[ALTAppNameErrorKey] = processName;
|
|
||||||
userInfo[ALTDeviceNameErrorKey] = self.device.name;
|
|
||||||
|
|
||||||
NSError *returnError = [NSError errorWithDomain:error.domain code:error.code userInfo:userInfo];
|
|
||||||
return completionHandler(NO, returnError);
|
|
||||||
}
|
|
||||||
|
|
||||||
completionHandler(YES, nil);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - Private -
|
|
||||||
|
|
||||||
- (BOOL)sendCommand:(NSString *)command arguments:(nullable NSArray<NSString *> *)arguments error:(NSError **)error
|
|
||||||
{
|
|
||||||
int argc = (int)arguments.count;
|
|
||||||
char **argv = new char*[argc + 1];
|
|
||||||
|
|
||||||
for (int i = 0; i < argc; i++)
|
|
||||||
{
|
|
||||||
NSString *argument = arguments[i];
|
|
||||||
argv[i] = (char *)argument.UTF8String;
|
|
||||||
}
|
|
||||||
|
|
||||||
argv[argc] = NULL;
|
|
||||||
|
|
||||||
debugserver_command_t debugCommand = NULL;
|
|
||||||
debugserver_command_new(command.UTF8String, argc, argv, &debugCommand);
|
|
||||||
|
|
||||||
delete[] argv;
|
|
||||||
|
|
||||||
char *response = NULL;
|
|
||||||
size_t responseSize = 0;
|
|
||||||
debugserver_error_t debugServerError = debugserver_client_send_command(self.client, debugCommand, &response, &responseSize);
|
|
||||||
debugserver_command_free(debugCommand);
|
|
||||||
|
|
||||||
if (debugServerError != DEBUGSERVER_E_SUCCESS)
|
|
||||||
{
|
|
||||||
if (error)
|
|
||||||
{
|
|
||||||
*error = [NSError errorWithDebugServerError:debugServerError device:self.device];
|
|
||||||
}
|
|
||||||
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSString *rawResponse = (response != nil) ? @(response) : nil;
|
|
||||||
if (![self processResponse:rawResponse error:error])
|
|
||||||
{
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)processResponse:(NSString *)rawResponse error:(NSError **)error
|
|
||||||
{
|
|
||||||
if (rawResponse == nil)
|
|
||||||
{
|
|
||||||
if (error)
|
|
||||||
{
|
|
||||||
*error = [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorRequestedAppNotRunning userInfo:nil];
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rawResponse.length == 0 || [rawResponse isEqualToString:@"OK"])
|
|
||||||
{
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
char type = [rawResponse characterAtIndex:0];
|
|
||||||
NSString *response = [rawResponse substringFromIndex:1];
|
|
||||||
|
|
||||||
switch (type)
|
|
||||||
{
|
|
||||||
case 'O':
|
|
||||||
{
|
|
||||||
// stdout/stderr
|
|
||||||
|
|
||||||
char *decodedResponse = NULL;
|
|
||||||
debugserver_decode_string(response.UTF8String, response.length, &decodedResponse);
|
|
||||||
|
|
||||||
NSLog(@"Response: %@", @(decodedResponse));
|
|
||||||
|
|
||||||
if (decodedResponse)
|
|
||||||
{
|
|
||||||
free(decodedResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'T':
|
|
||||||
{
|
|
||||||
// Thread Information
|
|
||||||
|
|
||||||
NSLog(@"Thread stopped. Details:\n%s", response.UTF8String + 1);
|
|
||||||
|
|
||||||
// Parse thread state to determine if app is running.
|
|
||||||
NSArray<NSString *> *components = [response componentsSeparatedByString:@";"];
|
|
||||||
if (components.count <= 1)
|
|
||||||
{
|
|
||||||
if (error)
|
|
||||||
{
|
|
||||||
*error = [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorUnknown userInfo:@{
|
|
||||||
NSLocalizedFailureReasonErrorKey: response,
|
|
||||||
ALTSourceFileErrorKey: @(__FILE__).lastPathComponent,
|
|
||||||
ALTSourceLineErrorKey: @(__LINE__)
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSString *mainThread = components[1];
|
|
||||||
NSString *threadState = [[mainThread componentsSeparatedByString:@":"] lastObject];
|
|
||||||
|
|
||||||
NSScanner *scanner = [NSScanner scannerWithString:threadState];
|
|
||||||
|
|
||||||
unsigned long long mainThreadState = 0;
|
|
||||||
[scanner scanHexLongLong:&mainThreadState];
|
|
||||||
|
|
||||||
// If main thread state == 0, app is not running.
|
|
||||||
if (mainThreadState == 0)
|
|
||||||
{
|
|
||||||
if (error)
|
|
||||||
{
|
|
||||||
*error = [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorRequestedAppNotRunning userInfo:nil];
|
|
||||||
}
|
|
||||||
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'E':
|
|
||||||
{
|
|
||||||
// Error
|
|
||||||
|
|
||||||
if (error)
|
|
||||||
{
|
|
||||||
NSInteger errorCode = [[[response componentsSeparatedByString:@";"] firstObject] integerValue];
|
|
||||||
|
|
||||||
switch (errorCode)
|
|
||||||
{
|
|
||||||
case 96:
|
|
||||||
*error = [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorRequestedAppNotRunning userInfo:nil];
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
*error = [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorUnknown userInfo:@{
|
|
||||||
NSLocalizedFailureReasonErrorKey: response,
|
|
||||||
ALTSourceFileErrorKey: @(__FILE__).lastPathComponent,
|
|
||||||
ALTSourceLineErrorKey: @(__LINE__)
|
|
||||||
}];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'W':
|
|
||||||
{
|
|
||||||
// Warning
|
|
||||||
|
|
||||||
NSLog(@"WARNING: %@", response);
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
//
|
|
||||||
// ALTNotificationConnection+Private.h
|
|
||||||
// AltServer
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 1/10/20.
|
|
||||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "ALTNotificationConnection.h"
|
|
||||||
|
|
||||||
#include <libimobiledevice/libimobiledevice.h>
|
|
||||||
#include <libimobiledevice/notification_proxy.h>
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
|
||||||
|
|
||||||
@interface ALTNotificationConnection ()
|
|
||||||
|
|
||||||
@property (nonatomic, readonly) np_client_t client;
|
|
||||||
|
|
||||||
- (instancetype)initWithDevice:(ALTDevice *)device client:(np_client_t)client;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
//
|
|
||||||
// ALTNotificationConnection.h
|
|
||||||
// AltServer
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 1/10/20.
|
|
||||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "AltSign.h"
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
|
||||||
|
|
||||||
NS_SWIFT_NAME(NotificationConnection)
|
|
||||||
@interface ALTNotificationConnection : NSObject
|
|
||||||
|
|
||||||
@property (nonatomic, copy, readonly) ALTDevice *device;
|
|
||||||
|
|
||||||
@property (nonatomic, copy, nullable) void (^receivedNotificationHandler)(CFNotificationName notification);
|
|
||||||
|
|
||||||
- (void)startListeningForNotifications:(NSArray<NSString *> *)notifications
|
|
||||||
completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
|
||||||
|
|
||||||
- (void)sendNotification:(CFNotificationName)notification
|
|
||||||
completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
|
||||||
|
|
||||||
- (void)disconnect;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
//
|
|
||||||
// ALTNotificationConnection.m
|
|
||||||
// AltServer
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 1/10/20.
|
|
||||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "ALTNotificationConnection+Private.h"
|
|
||||||
|
|
||||||
#import "NSError+ALTServerError.h"
|
|
||||||
|
|
||||||
void ALTDeviceReceivedNotification(const char *notification, void *user_data);
|
|
||||||
|
|
||||||
@implementation ALTNotificationConnection
|
|
||||||
|
|
||||||
- (instancetype)initWithDevice:(ALTDevice *)device client:(np_client_t)client
|
|
||||||
{
|
|
||||||
self = [super init];
|
|
||||||
if (self)
|
|
||||||
{
|
|
||||||
_device = [device copy];
|
|
||||||
_client = client;
|
|
||||||
}
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)dealloc
|
|
||||||
{
|
|
||||||
[self disconnect];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)disconnect
|
|
||||||
{
|
|
||||||
np_client_free(self.client);
|
|
||||||
_client = nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)startListeningForNotifications:(NSArray<NSString *> *)notifications completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler
|
|
||||||
{
|
|
||||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
|
||||||
const char **notificationNames = (const char **)malloc((notifications.count + 1) * sizeof(char *));
|
|
||||||
for (int i = 0; i < notifications.count; i++)
|
|
||||||
{
|
|
||||||
NSString *name = notifications[i];
|
|
||||||
notificationNames[i] = name.UTF8String;
|
|
||||||
}
|
|
||||||
notificationNames[notifications.count] = NULL; // Must have terminating NULL entry.
|
|
||||||
|
|
||||||
np_error_t result = np_observe_notifications(self.client, notificationNames);
|
|
||||||
if (result != NP_E_SUCCESS)
|
|
||||||
{
|
|
||||||
return completionHandler(NO, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]);
|
|
||||||
}
|
|
||||||
|
|
||||||
result = np_set_notify_callback(self.client, ALTDeviceReceivedNotification, (__bridge void *)self);
|
|
||||||
if (result != NP_E_SUCCESS)
|
|
||||||
{
|
|
||||||
return completionHandler(NO, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]);
|
|
||||||
}
|
|
||||||
|
|
||||||
completionHandler(YES, nil);
|
|
||||||
|
|
||||||
free(notificationNames);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)sendNotification:(CFNotificationName)notification completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler
|
|
||||||
{
|
|
||||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
|
||||||
np_error_t result = np_post_notification(self.client, [(__bridge NSString *)notification UTF8String]);
|
|
||||||
if (result == NP_E_SUCCESS)
|
|
||||||
{
|
|
||||||
completionHandler(YES, nil);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
completionHandler(NO, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
void ALTDeviceReceivedNotification(const char *notification, void *user_data)
|
|
||||||
{
|
|
||||||
ALTNotificationConnection *connection = (__bridge ALTNotificationConnection *)user_data;
|
|
||||||
|
|
||||||
if (connection.receivedNotificationHandler)
|
|
||||||
{
|
|
||||||
connection.receivedNotificationHandler((__bridge CFNotificationName)@(notification));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
//
|
|
||||||
// ALTWiredConnection+Private.h
|
|
||||||
// AltServer
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 1/10/20.
|
|
||||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "ALTWiredConnection.h"
|
|
||||||
|
|
||||||
#include <libimobiledevice/libimobiledevice.h>
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
|
||||||
|
|
||||||
@interface ALTWiredConnection ()
|
|
||||||
|
|
||||||
@property (nonatomic, readwrite, getter=isConnected) BOOL connected;
|
|
||||||
|
|
||||||
@property (nonatomic, readonly) idevice_connection_t connection;
|
|
||||||
|
|
||||||
- (instancetype)initWithDevice:(ALTDevice *)device connection:(idevice_connection_t)connection;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
//
|
|
||||||
// ALTWiredConnection.h
|
|
||||||
// AltServer
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 1/10/20.
|
|
||||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "AltSign.h"
|
|
||||||
|
|
||||||
#import "ALTConnection.h"
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
|
||||||
|
|
||||||
NS_SWIFT_NAME(WiredConnection)
|
|
||||||
@interface ALTWiredConnection : NSObject <ALTConnection>
|
|
||||||
|
|
||||||
@property (nonatomic, readonly, getter=isConnected) BOOL connected;
|
|
||||||
|
|
||||||
@property (nonatomic, copy, readonly) ALTDevice *device;
|
|
||||||
|
|
||||||
- (void)sendData:(NSData *)data completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler;
|
|
||||||
- (void)receiveDataWithExpectedSize:(NSInteger)expectedSize completionHandler:(void (^)(NSData * _Nullable, NSError * _Nullable))completionHandler;
|
|
||||||
|
|
||||||
- (void)disconnect;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
//
|
|
||||||
// ALTWiredConnection.m
|
|
||||||
// AltServer
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 1/10/20.
|
|
||||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "ALTWiredConnection+Private.h"
|
|
||||||
|
|
||||||
#import "ALTConnection.h"
|
|
||||||
#import "NSError+ALTServerError.h"
|
|
||||||
|
|
||||||
@implementation ALTWiredConnection
|
|
||||||
|
|
||||||
- (instancetype)initWithDevice:(ALTDevice *)device connection:(idevice_connection_t)connection
|
|
||||||
{
|
|
||||||
self = [super init];
|
|
||||||
if (self)
|
|
||||||
{
|
|
||||||
_device = [device copy];
|
|
||||||
_connection = connection;
|
|
||||||
|
|
||||||
self.connected = YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)dealloc
|
|
||||||
{
|
|
||||||
[self disconnect];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)disconnect
|
|
||||||
{
|
|
||||||
if (![self isConnected])
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
idevice_disconnect(self.connection);
|
|
||||||
_connection = nil;
|
|
||||||
|
|
||||||
self.connected = NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)sendData:(NSData *)data completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler
|
|
||||||
{
|
|
||||||
void (^finish)(NSError *error) = ^(NSError *error) {
|
|
||||||
if (error != nil)
|
|
||||||
{
|
|
||||||
NSLog(@"Send Error: %@", error);
|
|
||||||
completionHandler(NO, error);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
completionHandler(YES, nil);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
|
||||||
NSMutableData *mutableData = [data mutableCopy];
|
|
||||||
while (mutableData.length > 0)
|
|
||||||
{
|
|
||||||
uint32_t sentBytes = 0;
|
|
||||||
if (idevice_connection_send(self.connection, (const char *)mutableData.bytes, (int32_t)mutableData.length, &sentBytes) != IDEVICE_E_SUCCESS)
|
|
||||||
{
|
|
||||||
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]);
|
|
||||||
}
|
|
||||||
|
|
||||||
[mutableData replaceBytesInRange:NSMakeRange(0, sentBytes) withBytes:NULL length:0];
|
|
||||||
}
|
|
||||||
|
|
||||||
finish(nil);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)receiveDataWithExpectedSize:(NSInteger)expectedSize completionHandler:(void (^)(NSData * _Nullable, NSError * _Nullable))completionHandler
|
|
||||||
{
|
|
||||||
void (^finish)(NSData *data, NSError *error) = ^(NSData *data, NSError *error) {
|
|
||||||
if (error != nil)
|
|
||||||
{
|
|
||||||
NSLog(@"Receive Data Error: %@", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
completionHandler(data, error);
|
|
||||||
};
|
|
||||||
|
|
||||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
|
||||||
char bytes[4096];
|
|
||||||
NSMutableData *receivedData = [NSMutableData dataWithCapacity:expectedSize];
|
|
||||||
|
|
||||||
while (receivedData.length < expectedSize)
|
|
||||||
{
|
|
||||||
uint32_t size = MIN(4096, (uint32_t)expectedSize - (uint32_t)receivedData.length);
|
|
||||||
|
|
||||||
uint32_t receivedBytes = 0;
|
|
||||||
if (idevice_connection_receive_timeout(self.connection, bytes, size, &receivedBytes, 10000) != IDEVICE_E_SUCCESS)
|
|
||||||
{
|
|
||||||
return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]);
|
|
||||||
}
|
|
||||||
|
|
||||||
NSData *data = [NSData dataWithBytesNoCopy:bytes length:receivedBytes freeWhenDone:NO];
|
|
||||||
[receivedData appendData:data];
|
|
||||||
}
|
|
||||||
|
|
||||||
finish(receivedData, nil);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - NSObject -
|
|
||||||
|
|
||||||
- (NSString *)description
|
|
||||||
{
|
|
||||||
return [NSString stringWithFormat:@"%@ (Wired)", self.device.name];
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
//
|
|
||||||
// RequestHandler.swift
|
|
||||||
// AltServer
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 5/23/19.
|
|
||||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
typealias ServerConnectionManager = ConnectionManager<ServerRequestHandler>
|
|
||||||
|
|
||||||
private let connectionManager = ConnectionManager(requestHandler: ServerRequestHandler(),
|
|
||||||
connectionHandlers: [WirelessConnectionHandler(), WiredConnectionHandler()])
|
|
||||||
|
|
||||||
extension ServerConnectionManager
|
|
||||||
{
|
|
||||||
static var shared: ConnectionManager {
|
|
||||||
return connectionManager
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ServerRequestHandler: RequestHandler
|
|
||||||
{
|
|
||||||
func handleAnisetteDataRequest(_ request: AnisetteDataRequest, for connection: Connection, completionHandler: @escaping (Result<AnisetteDataResponse, Error>) -> Void)
|
|
||||||
{
|
|
||||||
AnisetteDataManager.shared.requestAnisetteData { (result) in
|
|
||||||
switch result
|
|
||||||
{
|
|
||||||
case .failure(let error): completionHandler(.failure(error))
|
|
||||||
case .success(let anisetteData):
|
|
||||||
let response = AnisetteDataResponse(anisetteData: anisetteData)
|
|
||||||
completionHandler(.success(response))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: Connection, completionHandler: @escaping (Result<InstallationProgressResponse, Error>) -> Void)
|
|
||||||
{
|
|
||||||
var temporaryURL: URL?
|
|
||||||
|
|
||||||
func finish(_ result: Result<InstallationProgressResponse, Error>)
|
|
||||||
{
|
|
||||||
if let temporaryURL = temporaryURL
|
|
||||||
{
|
|
||||||
do { try FileManager.default.removeItem(at: temporaryURL) }
|
|
||||||
catch { print("Failed to remove .ipa.", error) }
|
|
||||||
}
|
|
||||||
|
|
||||||
completionHandler(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.receiveApp(for: request, from: connection) { (result) in
|
|
||||||
print("Received app with result:", result)
|
|
||||||
|
|
||||||
switch result
|
|
||||||
{
|
|
||||||
case .failure(let error): finish(.failure(error))
|
|
||||||
case .success(let fileURL):
|
|
||||||
temporaryURL = fileURL
|
|
||||||
|
|
||||||
print("Awaiting begin installation request...")
|
|
||||||
|
|
||||||
connection.receiveRequest() { (result) in
|
|
||||||
print("Received begin installation request with result:", result)
|
|
||||||
|
|
||||||
switch result
|
|
||||||
{
|
|
||||||
case .failure(let error): finish(.failure(error))
|
|
||||||
case .success(.beginInstallation(let installRequest)):
|
|
||||||
print("Installing app to device \(request.udid)...")
|
|
||||||
|
|
||||||
self.installApp(at: fileURL, toDeviceWithUDID: request.udid, activeProvisioningProfiles: installRequest.activeProfiles, connection: connection) { (result) in
|
|
||||||
print("Installed app to device with result:", result)
|
|
||||||
switch result
|
|
||||||
{
|
|
||||||
case .failure(let error): finish(.failure(error))
|
|
||||||
case .success:
|
|
||||||
let response = InstallationProgressResponse(progress: 1.0)
|
|
||||||
finish(.success(response))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case .success: finish(.failure(ALTServerError(.unknownRequest)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleInstallProvisioningProfilesRequest(_ request: InstallProvisioningProfilesRequest, for connection: Connection,
|
|
||||||
completionHandler: @escaping (Result<InstallProvisioningProfilesResponse, Error>) -> Void)
|
|
||||||
{
|
|
||||||
ALTDeviceManager.shared.installProvisioningProfiles(request.provisioningProfiles, toDeviceWithUDID: request.udid, activeProvisioningProfiles: request.activeProfiles) { (success, error) in
|
|
||||||
if let error = error, !success
|
|
||||||
{
|
|
||||||
print("Failed to install profiles \(request.provisioningProfiles.map { $0.bundleIdentifier }):", error)
|
|
||||||
completionHandler(.failure(ALTServerError(error)))
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
print("Installed profiles:", request.provisioningProfiles.map { $0.bundleIdentifier })
|
|
||||||
|
|
||||||
let response = InstallProvisioningProfilesResponse()
|
|
||||||
completionHandler(.success(response))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleRemoveProvisioningProfilesRequest(_ request: RemoveProvisioningProfilesRequest, for connection: Connection,
|
|
||||||
completionHandler: @escaping (Result<RemoveProvisioningProfilesResponse, Error>) -> Void)
|
|
||||||
{
|
|
||||||
ALTDeviceManager.shared.removeProvisioningProfiles(forBundleIdentifiers: request.bundleIdentifiers, fromDeviceWithUDID: request.udid) { (success, error) in
|
|
||||||
if let error = error, !success
|
|
||||||
{
|
|
||||||
print("Failed to remove profiles \(request.bundleIdentifiers):", error)
|
|
||||||
completionHandler(.failure(ALTServerError(error)))
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
print("Removed profiles:", request.bundleIdentifiers)
|
|
||||||
|
|
||||||
let response = RemoveProvisioningProfilesResponse()
|
|
||||||
completionHandler(.success(response))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleRemoveAppRequest(_ request: RemoveAppRequest, for connection: Connection, completionHandler: @escaping (Result<RemoveAppResponse, Error>) -> Void)
|
|
||||||
{
|
|
||||||
ALTDeviceManager.shared.removeApp(forBundleIdentifier: request.bundleIdentifier, fromDeviceWithUDID: request.udid) { (success, error) in
|
|
||||||
if let error = error, !success
|
|
||||||
{
|
|
||||||
print("Failed to remove app \(request.bundleIdentifier):", error)
|
|
||||||
completionHandler(.failure(ALTServerError(error)))
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
print("Removed app:", request.bundleIdentifier)
|
|
||||||
|
|
||||||
let response = RemoveAppResponse()
|
|
||||||
completionHandler(.success(response))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleEnableUnsignedCodeExecutionRequest(_ request: EnableUnsignedCodeExecutionRequest, for connection: Connection, completionHandler: @escaping (Result<EnableUnsignedCodeExecutionResponse, Error>) -> Void)
|
|
||||||
{
|
|
||||||
guard let device = ALTDeviceManager.shared.availableDevices.first(where: { $0.identifier == request.udid }) else { return completionHandler(.failure(ALTServerError(.deviceNotFound))) }
|
|
||||||
|
|
||||||
ALTDeviceManager.shared.prepare(device) { result in
|
|
||||||
switch result
|
|
||||||
{
|
|
||||||
case .failure(let error): completionHandler(.failure(error))
|
|
||||||
case .success:
|
|
||||||
ALTDeviceManager.shared.startDebugConnection(to: device) { (connection, error) in
|
|
||||||
guard let connection = connection else { return completionHandler(.failure(error!)) }
|
|
||||||
|
|
||||||
func finish(success: Bool, error: Error?)
|
|
||||||
{
|
|
||||||
if let error = error, !success
|
|
||||||
{
|
|
||||||
print("Failed to enable unsigned code execution for process \(request.processID?.description ?? request.processName ?? "nil"):", error)
|
|
||||||
completionHandler(.failure(ALTServerError(error)))
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
print("Enabled unsigned code execution for process:", request.processID ?? request.processName ?? "nil")
|
|
||||||
|
|
||||||
let response = EnableUnsignedCodeExecutionResponse()
|
|
||||||
completionHandler(.success(response))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let processID = request.processID
|
|
||||||
{
|
|
||||||
connection.enableUnsignedCodeExecutionForProcess(withID: processID, completionHandler: finish)
|
|
||||||
}
|
|
||||||
else if let processName = request.processName
|
|
||||||
{
|
|
||||||
connection.enableUnsignedCodeExecutionForProcess(withName: processName, completionHandler: finish)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
finish(success: false, error: ALTServerError(.invalidRequest))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension RequestHandler
|
|
||||||
{
|
|
||||||
func receiveApp(for request: PrepareAppRequest, from connection: Connection, completionHandler: @escaping (Result<URL, ALTServerError>) -> Void)
|
|
||||||
{
|
|
||||||
connection.receiveData(expectedSize: request.contentSize) { (result) in
|
|
||||||
do
|
|
||||||
{
|
|
||||||
print("Received app data!")
|
|
||||||
|
|
||||||
let data = try result.get()
|
|
||||||
|
|
||||||
guard ALTDeviceManager.shared.availableDevices.contains(where: { $0.identifier == request.udid }) else { throw ALTServerError(.deviceNotFound) }
|
|
||||||
|
|
||||||
print("Writing app data...")
|
|
||||||
|
|
||||||
let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".ipa")
|
|
||||||
try data.write(to: temporaryURL, options: .atomic)
|
|
||||||
|
|
||||||
print("Wrote app to URL:", temporaryURL)
|
|
||||||
|
|
||||||
completionHandler(.success(temporaryURL))
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
print("Error processing app data:", error)
|
|
||||||
|
|
||||||
completionHandler(.failure(ALTServerError(error)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func installApp(at fileURL: URL, toDeviceWithUDID udid: String, activeProvisioningProfiles: Set<String>?, connection: Connection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
|
|
||||||
{
|
|
||||||
let serialQueue = DispatchQueue(label: "com.altstore.ConnectionManager.installQueue", qos: .default)
|
|
||||||
var isSending = false
|
|
||||||
|
|
||||||
var observation: NSKeyValueObservation?
|
|
||||||
|
|
||||||
let progress = ALTDeviceManager.shared.installApp(at: fileURL, toDeviceWithUDID: udid, activeProvisioningProfiles: activeProvisioningProfiles) { (success, error) in
|
|
||||||
print("Installed app with result:", error == nil ? "Success" : error!.localizedDescription)
|
|
||||||
|
|
||||||
if let error = error.map({ ALTServerError($0) })
|
|
||||||
{
|
|
||||||
completionHandler(.failure(error))
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
completionHandler(.success(()))
|
|
||||||
}
|
|
||||||
|
|
||||||
observation?.invalidate()
|
|
||||||
observation = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
observation = progress.observe(\.fractionCompleted, changeHandler: { (progress, change) in
|
|
||||||
serialQueue.async {
|
|
||||||
guard !isSending else { return }
|
|
||||||
isSending = true
|
|
||||||
|
|
||||||
print("Progress:", progress.fractionCompleted)
|
|
||||||
let response = InstallationProgressResponse(progress: progress.fractionCompleted)
|
|
||||||
|
|
||||||
connection.send(response) { (result) in
|
|
||||||
serialQueue.async {
|
|
||||||
isSending = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
//
|
|
||||||
// WiredConnectionHandler.swift
|
|
||||||
// AltServer
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 6/1/20.
|
|
||||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
class WiredConnectionHandler: ConnectionHandler
|
|
||||||
{
|
|
||||||
var connectionHandler: ((Connection) -> Void)?
|
|
||||||
var disconnectionHandler: ((Connection) -> Void)?
|
|
||||||
|
|
||||||
private var notificationConnections = [ALTDevice: NotificationConnection]()
|
|
||||||
private let queue = DispatchQueue(label: "WiredConnectionHandler", autoreleaseFrequency: .workItem, target: .global(qos: .utility))
|
|
||||||
|
|
||||||
func startListening()
|
|
||||||
{
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(WiredConnectionHandler.deviceDidConnect(_:)), name: .deviceManagerDeviceDidConnect, object: nil)
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(WiredConnectionHandler.deviceDidDisconnect(_:)), name: .deviceManagerDeviceDidDisconnect, object: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopListening()
|
|
||||||
{
|
|
||||||
NotificationCenter.default.removeObserver(self, name: .deviceManagerDeviceDidConnect, object: nil)
|
|
||||||
NotificationCenter.default.removeObserver(self, name: .deviceManagerDeviceDidDisconnect, object: nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension WiredConnectionHandler
|
|
||||||
{
|
|
||||||
func startNotificationConnection(to device: ALTDevice)
|
|
||||||
{
|
|
||||||
self.queue.async {
|
|
||||||
ALTDeviceManager.shared.startNotificationConnection(to: device) { (connection, error) in
|
|
||||||
guard let connection = connection else { return }
|
|
||||||
|
|
||||||
let notifications: [CFNotificationName] = [.wiredServerConnectionAvailableRequest, .wiredServerConnectionStartRequest]
|
|
||||||
connection.startListening(forNotifications: notifications.map { String($0.rawValue) }) { (success, error) in
|
|
||||||
guard success else { return }
|
|
||||||
|
|
||||||
self.queue.async {
|
|
||||||
connection.receivedNotificationHandler = { [weak self, weak connection] (notification) in
|
|
||||||
guard let self = self, let connection = connection else { return }
|
|
||||||
self.handle(notification, for: connection)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.notificationConnections[device] = connection
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopNotificationConnection(to device: ALTDevice)
|
|
||||||
{
|
|
||||||
self.queue.async {
|
|
||||||
guard let connection = self.notificationConnections[device] else { return }
|
|
||||||
connection.disconnect()
|
|
||||||
|
|
||||||
self.notificationConnections[device] = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handle(_ notification: CFNotificationName, for connection: NotificationConnection)
|
|
||||||
{
|
|
||||||
switch notification
|
|
||||||
{
|
|
||||||
case .wiredServerConnectionAvailableRequest:
|
|
||||||
connection.sendNotification(.wiredServerConnectionAvailableResponse) { (success, error) in
|
|
||||||
if let error = error, !success
|
|
||||||
{
|
|
||||||
print("Error sending wired server connection response.", error)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
print("Sent wired server connection available response!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case .wiredServerConnectionStartRequest:
|
|
||||||
ALTDeviceManager.shared.startWiredConnection(to: connection.device) { (wiredConnection, error) in
|
|
||||||
if let wiredConnection = wiredConnection
|
|
||||||
{
|
|
||||||
print("Started wired server connection!")
|
|
||||||
self.connectionHandler?(wiredConnection)
|
|
||||||
|
|
||||||
var observation: NSKeyValueObservation?
|
|
||||||
observation = wiredConnection.observe(\.isConnected) { [weak self] (connection, change) in
|
|
||||||
guard !connection.isConnected else { return }
|
|
||||||
self?.disconnectionHandler?(connection)
|
|
||||||
|
|
||||||
observation?.invalidate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if let error = error
|
|
||||||
{
|
|
||||||
print("Error starting wired server connection.", error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
default: break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension WiredConnectionHandler
|
|
||||||
{
|
|
||||||
@objc func deviceDidConnect(_ notification: Notification)
|
|
||||||
{
|
|
||||||
guard let device = notification.object as? ALTDevice else { return }
|
|
||||||
self.startNotificationConnection(to: device)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func deviceDidDisconnect(_ notification: Notification)
|
|
||||||
{
|
|
||||||
guard let device = notification.object as? ALTDevice else { return }
|
|
||||||
self.stopNotificationConnection(to: device)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
//
|
|
||||||
// WirelessConnectionHandler.swift
|
|
||||||
// AltKit
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 6/1/20.
|
|
||||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Network
|
|
||||||
|
|
||||||
extension WirelessConnectionHandler
|
|
||||||
{
|
|
||||||
public enum State
|
|
||||||
{
|
|
||||||
case notRunning
|
|
||||||
case connecting
|
|
||||||
case running(NWListener.Service)
|
|
||||||
case failed(Swift.Error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class WirelessConnectionHandler: ConnectionHandler
|
|
||||||
{
|
|
||||||
public var connectionHandler: ((Connection) -> Void)?
|
|
||||||
public var disconnectionHandler: ((Connection) -> Void)?
|
|
||||||
|
|
||||||
public var stateUpdateHandler: ((State) -> Void)?
|
|
||||||
|
|
||||||
public private(set) var state: State = .notRunning {
|
|
||||||
didSet {
|
|
||||||
self.stateUpdateHandler?(self.state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private lazy var listener = self.makeListener()
|
|
||||||
private let dispatchQueue = DispatchQueue(label: "io.altstore.WirelessConnectionListener", qos: .utility)
|
|
||||||
|
|
||||||
public func startListening()
|
|
||||||
{
|
|
||||||
switch self.state
|
|
||||||
{
|
|
||||||
case .notRunning, .failed: self.listener.start(queue: self.dispatchQueue)
|
|
||||||
default: break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func stopListening()
|
|
||||||
{
|
|
||||||
switch self.state
|
|
||||||
{
|
|
||||||
case .running: self.listener.cancel()
|
|
||||||
default: break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension WirelessConnectionHandler
|
|
||||||
{
|
|
||||||
func makeListener() -> NWListener
|
|
||||||
{
|
|
||||||
let listener = try! NWListener(using: .tcp)
|
|
||||||
|
|
||||||
let service: NWListener.Service
|
|
||||||
|
|
||||||
if let serverID = UserDefaults.standard.serverID?.data(using: .utf8)
|
|
||||||
{
|
|
||||||
let txtDictionary = ["serverID": serverID]
|
|
||||||
let txtData = NetService.data(fromTXTRecord: txtDictionary)
|
|
||||||
|
|
||||||
service = NWListener.Service(name: nil, type: ALTServerServiceType, domain: nil, txtRecord: txtData)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
service = NWListener.Service(type: ALTServerServiceType)
|
|
||||||
}
|
|
||||||
|
|
||||||
listener.service = service
|
|
||||||
|
|
||||||
listener.serviceRegistrationUpdateHandler = { (serviceChange) in
|
|
||||||
switch serviceChange
|
|
||||||
{
|
|
||||||
case .add(.service(let name, let type, let domain, _)):
|
|
||||||
let service = NWListener.Service(name: name, type: type, domain: domain, txtRecord: nil)
|
|
||||||
self.state = .running(service)
|
|
||||||
|
|
||||||
default: break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
listener.stateUpdateHandler = { (state) in
|
|
||||||
switch state
|
|
||||||
{
|
|
||||||
case .ready: break
|
|
||||||
case .waiting, .setup: self.state = .connecting
|
|
||||||
case .cancelled: self.state = .notRunning
|
|
||||||
case .failed(let error): self.state = .failed(error)
|
|
||||||
@unknown default: break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
listener.newConnectionHandler = { [weak self] (connection) in
|
|
||||||
self?.prepare(connection)
|
|
||||||
}
|
|
||||||
|
|
||||||
return listener
|
|
||||||
}
|
|
||||||
|
|
||||||
func prepare(_ nwConnection: NWConnection)
|
|
||||||
{
|
|
||||||
print("Preparing:", nwConnection)
|
|
||||||
|
|
||||||
// Use same instance for all callbacks.
|
|
||||||
let connection = NetworkConnection(nwConnection)
|
|
||||||
|
|
||||||
nwConnection.stateUpdateHandler = { [weak self] (state) in
|
|
||||||
switch state
|
|
||||||
{
|
|
||||||
case .setup, .preparing: break
|
|
||||||
|
|
||||||
case .ready:
|
|
||||||
print("Connected to client:", connection)
|
|
||||||
self?.connectionHandler?(connection)
|
|
||||||
|
|
||||||
case .waiting:
|
|
||||||
print("Waiting for connection...")
|
|
||||||
|
|
||||||
case .failed(let error):
|
|
||||||
print("Failed to connect to service \(nwConnection.endpoint).", error)
|
|
||||||
self?.disconnect(connection)
|
|
||||||
|
|
||||||
case .cancelled:
|
|
||||||
self?.disconnect(connection)
|
|
||||||
|
|
||||||
@unknown default: break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nwConnection.start(queue: self.dispatchQueue)
|
|
||||||
}
|
|
||||||
|
|
||||||
func disconnect(_ connection: Connection)
|
|
||||||
{
|
|
||||||
connection.disconnect()
|
|
||||||
|
|
||||||
self.disconnectionHandler?(connection)
|
|
||||||
|
|
||||||
if let networkConnection = connection as? NetworkConnection
|
|
||||||
{
|
|
||||||
networkConnection.nwConnection.stateUpdateHandler = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,328 +0,0 @@
|
|||||||
//
|
|
||||||
// DeveloperDiskManager.swift
|
|
||||||
// AltServer
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 2/19/21.
|
|
||||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
import AltSign
|
|
||||||
|
|
||||||
typealias DeveloperDiskError = DeveloperDiskErrorCode.Error
|
|
||||||
enum DeveloperDiskErrorCode: Int, ALTErrorEnum
|
|
||||||
{
|
|
||||||
case unknownDownloadURL
|
|
||||||
case unsupportedOperatingSystem
|
|
||||||
case downloadedDiskNotFound
|
|
||||||
|
|
||||||
var errorFailureReason: String {
|
|
||||||
switch self
|
|
||||||
{
|
|
||||||
case .unknownDownloadURL: return NSLocalizedString("The URL to download the Developer disk image could not be determined.", comment: "")
|
|
||||||
case .unsupportedOperatingSystem: return NSLocalizedString("The device's operating system does not support installing Developer disk images.", comment: "")
|
|
||||||
case .downloadedDiskNotFound: return NSLocalizedString("DeveloperDiskImage.dmg and its signature could not be found in the downloaded archive.", comment: "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension URL
|
|
||||||
{
|
|
||||||
#if STAGING
|
|
||||||
static let developerDiskDownloadURLs = URL(string: "https://f000.backblazeb2.com/file/altstore-staging/altserver/developerdisks.json")!
|
|
||||||
#else
|
|
||||||
static let developerDiskDownloadURLs = URL(string: "https://cdn.altstore.io/file/altstore/altserver/developerdisks.json")!
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension DeveloperDiskManager
|
|
||||||
{
|
|
||||||
struct FetchURLsResponse: Decodable
|
|
||||||
{
|
|
||||||
struct Disks: Decodable
|
|
||||||
{
|
|
||||||
var iOS: [String: DeveloperDiskURL]?
|
|
||||||
var tvOS: [String: DeveloperDiskURL]?
|
|
||||||
}
|
|
||||||
|
|
||||||
var version: Int
|
|
||||||
var disks: Disks
|
|
||||||
}
|
|
||||||
|
|
||||||
enum DeveloperDiskURL: Decodable
|
|
||||||
{
|
|
||||||
case archive(URL)
|
|
||||||
case separate(diskURL: URL, signatureURL: URL)
|
|
||||||
|
|
||||||
private enum CodingKeys: CodingKey
|
|
||||||
{
|
|
||||||
case archive
|
|
||||||
case disk
|
|
||||||
case signature
|
|
||||||
}
|
|
||||||
|
|
||||||
init(from decoder: Decoder) throws
|
|
||||||
{
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
|
|
||||||
if container.contains(.archive)
|
|
||||||
{
|
|
||||||
let archiveURL = try container.decode(URL.self, forKey: .archive)
|
|
||||||
self = .archive(archiveURL)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
let diskURL = try container.decode(URL.self, forKey: .disk)
|
|
||||||
let signatureURL = try container.decode(URL.self, forKey: .signature)
|
|
||||||
self = .separate(diskURL: diskURL, signatureURL: signatureURL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DeveloperDiskManager
|
|
||||||
{
|
|
||||||
private let session = URLSession(configuration: .ephemeral)
|
|
||||||
|
|
||||||
func downloadDeveloperDisk(for device: ALTDevice, completionHandler: @escaping (Result<(URL, URL), Error>) -> Void)
|
|
||||||
{
|
|
||||||
do
|
|
||||||
{
|
|
||||||
guard let osName = device.type.osName else { throw DeveloperDiskError(.unsupportedOperatingSystem) }
|
|
||||||
|
|
||||||
let osKeyPath: KeyPath<FetchURLsResponse.Disks, [String: DeveloperDiskURL]?>
|
|
||||||
switch device.type
|
|
||||||
{
|
|
||||||
case .iphone, .ipad: osKeyPath = \FetchURLsResponse.Disks.iOS
|
|
||||||
case .appletv: osKeyPath = \FetchURLsResponse.Disks.tvOS
|
|
||||||
default: throw DeveloperDiskError(.unsupportedOperatingSystem)
|
|
||||||
}
|
|
||||||
|
|
||||||
var osVersion = device.osVersion
|
|
||||||
osVersion.patchVersion = 0 // Patch is irrelevant for developer disks
|
|
||||||
|
|
||||||
let osDirectoryURL = FileManager.default.developerDisksDirectory.appendingPathComponent(osName)
|
|
||||||
let developerDiskDirectoryURL = osDirectoryURL.appendingPathComponent(osVersion.stringValue, isDirectory: true)
|
|
||||||
try FileManager.default.createDirectory(at: developerDiskDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
|
||||||
|
|
||||||
let developerDiskURL = developerDiskDirectoryURL.appendingPathComponent("DeveloperDiskImage.dmg")
|
|
||||||
let developerDiskSignatureURL = developerDiskDirectoryURL.appendingPathComponent("DeveloperDiskImage.dmg.signature")
|
|
||||||
|
|
||||||
let isCachedDiskCompatible = self.isDeveloperDiskCompatible(with: device)
|
|
||||||
if isCachedDiskCompatible && FileManager.default.fileExists(atPath: developerDiskURL.path) && FileManager.default.fileExists(atPath: developerDiskSignatureURL.path)
|
|
||||||
{
|
|
||||||
// The developer disk is cached and we've confirmed it works, so re-use it.
|
|
||||||
return completionHandler(.success((developerDiskURL, developerDiskSignatureURL)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func finish(_ result: Result<(URL, URL), Error>)
|
|
||||||
{
|
|
||||||
do
|
|
||||||
{
|
|
||||||
let (diskFileURL, signatureFileURL) = try result.get()
|
|
||||||
|
|
||||||
if FileManager.default.fileExists(atPath: developerDiskURL.path)
|
|
||||||
{
|
|
||||||
try FileManager.default.removeItem(at: developerDiskURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
if FileManager.default.fileExists(atPath: developerDiskSignatureURL.path)
|
|
||||||
{
|
|
||||||
try FileManager.default.removeItem(at: developerDiskSignatureURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
try FileManager.default.copyItem(at: diskFileURL, to: developerDiskURL)
|
|
||||||
try FileManager.default.copyItem(at: signatureFileURL, to: developerDiskSignatureURL)
|
|
||||||
|
|
||||||
completionHandler(.success((developerDiskURL, developerDiskSignatureURL)))
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
completionHandler(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.fetchDeveloperDiskURLs { (result) in
|
|
||||||
do
|
|
||||||
{
|
|
||||||
let developerDiskURLs = try result.get()
|
|
||||||
guard let diskURL = developerDiskURLs[keyPath: osKeyPath]?[osVersion.stringValue] else { throw DeveloperDiskError(.unknownDownloadURL) }
|
|
||||||
|
|
||||||
switch diskURL
|
|
||||||
{
|
|
||||||
case .archive(let archiveURL): self.downloadDiskArchive(from: archiveURL, completionHandler: finish(_:))
|
|
||||||
case .separate(let diskURL, let signatureURL): self.downloadDisk(from: diskURL, signatureURL: signatureURL, completionHandler: finish(_:))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
finish(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
completionHandler(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setDeveloperDiskCompatible(_ isCompatible: Bool, with device: ALTDevice)
|
|
||||||
{
|
|
||||||
guard let id = self.developerDiskCompatibilityID(for: device) else { return }
|
|
||||||
UserDefaults.standard.set(isCompatible, forKey: id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isDeveloperDiskCompatible(with device: ALTDevice) -> Bool
|
|
||||||
{
|
|
||||||
guard let id = self.developerDiskCompatibilityID(for: device) else { return false }
|
|
||||||
|
|
||||||
let isCompatible = UserDefaults.standard.bool(forKey: id)
|
|
||||||
return isCompatible
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension DeveloperDiskManager
|
|
||||||
{
|
|
||||||
func developerDiskCompatibilityID(for device: ALTDevice) -> String?
|
|
||||||
{
|
|
||||||
guard let osName = device.type.osName else { return nil }
|
|
||||||
|
|
||||||
var osVersion = device.osVersion
|
|
||||||
osVersion.patchVersion = 0 // Patch is irrelevant for developer disks
|
|
||||||
|
|
||||||
let id = ["ALTDeveloperDiskCompatible", osName, device.osVersion.stringValue].joined(separator: "_")
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchDeveloperDiskURLs(completionHandler: @escaping (Result<FetchURLsResponse.Disks, Error>) -> Void)
|
|
||||||
{
|
|
||||||
let dataTask = self.session.dataTask(with: .developerDiskDownloadURLs) { (data, response, error) in
|
|
||||||
do
|
|
||||||
{
|
|
||||||
guard let data = data else { throw error! }
|
|
||||||
|
|
||||||
let response = try Foundation.JSONDecoder().decode(FetchURLsResponse.self, from: data)
|
|
||||||
completionHandler(.success(response.disks))
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
completionHandler(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dataTask.resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
func downloadDiskArchive(from url: URL, completionHandler: @escaping (Result<(URL, URL), Error>) -> Void)
|
|
||||||
{
|
|
||||||
let downloadTask = URLSession.shared.downloadTask(with: url) { (fileURL, response, error) in
|
|
||||||
do
|
|
||||||
{
|
|
||||||
guard let fileURL = fileURL else { throw error! }
|
|
||||||
defer { try? FileManager.default.removeItem(at: fileURL) }
|
|
||||||
|
|
||||||
let temporaryDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
||||||
try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
|
|
||||||
defer { try? FileManager.default.removeItem(at: temporaryDirectory) }
|
|
||||||
|
|
||||||
try FileManager.default.unzipArchive(at: fileURL, toDirectory: temporaryDirectory)
|
|
||||||
|
|
||||||
guard let enumerator = FileManager.default.enumerator(at: temporaryDirectory, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles, .skipsPackageDescendants]) else {
|
|
||||||
throw CocoaError(.fileNoSuchFile, userInfo: [NSURLErrorKey: temporaryDirectory])
|
|
||||||
}
|
|
||||||
|
|
||||||
var tempDiskFileURL: URL?
|
|
||||||
var tempSignatureFileURL: URL?
|
|
||||||
|
|
||||||
for case let fileURL as URL in enumerator
|
|
||||||
{
|
|
||||||
switch fileURL.pathExtension.lowercased()
|
|
||||||
{
|
|
||||||
case "dmg": tempDiskFileURL = fileURL
|
|
||||||
case "signature": tempSignatureFileURL = fileURL
|
|
||||||
default: break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let diskFileURL = tempDiskFileURL, let signatureFileURL = tempSignatureFileURL else { throw DeveloperDiskError(.downloadedDiskNotFound) }
|
|
||||||
|
|
||||||
completionHandler(.success((diskFileURL, signatureFileURL)))
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
completionHandler(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadTask.resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
func downloadDisk(from diskURL: URL, signatureURL: URL, completionHandler: @escaping (Result<(URL, URL), Error>) -> Void)
|
|
||||||
{
|
|
||||||
let temporaryDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
||||||
|
|
||||||
do { try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil) }
|
|
||||||
catch { return completionHandler(.failure(error)) }
|
|
||||||
|
|
||||||
var diskFileURL: URL?
|
|
||||||
var signatureFileURL: URL?
|
|
||||||
|
|
||||||
var downloadError: Error?
|
|
||||||
|
|
||||||
let dispatchGroup = DispatchGroup()
|
|
||||||
dispatchGroup.enter()
|
|
||||||
dispatchGroup.enter()
|
|
||||||
|
|
||||||
let diskDownloadTask = URLSession.shared.downloadTask(with: diskURL) { (fileURL, response, error) in
|
|
||||||
do
|
|
||||||
{
|
|
||||||
guard let fileURL = fileURL else { throw error! }
|
|
||||||
|
|
||||||
let destinationURL = temporaryDirectory.appendingPathComponent("DeveloperDiskImage.dmg")
|
|
||||||
try FileManager.default.copyItem(at: fileURL, to: destinationURL)
|
|
||||||
|
|
||||||
diskFileURL = destinationURL
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
downloadError = error
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatchGroup.leave()
|
|
||||||
}
|
|
||||||
|
|
||||||
let signatureDownloadTask = URLSession.shared.downloadTask(with: signatureURL) { (fileURL, response, error) in
|
|
||||||
do
|
|
||||||
{
|
|
||||||
guard let fileURL = fileURL else { throw error! }
|
|
||||||
|
|
||||||
let destinationURL = temporaryDirectory.appendingPathComponent("DeveloperDiskImage.dmg.signature")
|
|
||||||
try FileManager.default.copyItem(at: fileURL, to: destinationURL)
|
|
||||||
|
|
||||||
signatureFileURL = destinationURL
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
downloadError = error
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatchGroup.leave()
|
|
||||||
}
|
|
||||||
|
|
||||||
diskDownloadTask.resume()
|
|
||||||
signatureDownloadTask.resume()
|
|
||||||
|
|
||||||
dispatchGroup.notify(queue: .global(qos: .userInitiated)) {
|
|
||||||
defer {
|
|
||||||
try? FileManager.default.removeItem(at: temporaryDirectory)
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let diskFileURL = diskFileURL, let signatureFileURL = signatureFileURL else {
|
|
||||||
return completionHandler(.failure(downloadError ?? DeveloperDiskError(.downloadedDiskNotFound)))
|
|
||||||
}
|
|
||||||
|
|
||||||
completionHandler(.success((diskFileURL, signatureFileURL)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
//
|
|
||||||
// ALTDeviceManager.h
|
|
||||||
// AltServer
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 5/24/19.
|
|
||||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import "AltSign.h"
|
|
||||||
|
|
||||||
@class ALTWiredConnection;
|
|
||||||
@class ALTNotificationConnection;
|
|
||||||
@class ALTDebugConnection;
|
|
||||||
|
|
||||||
@class ALTInstalledApp;
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
|
||||||
|
|
||||||
extern NSNotificationName const ALTDeviceManagerDeviceDidConnectNotification NS_SWIFT_NAME(deviceManagerDeviceDidConnect);
|
|
||||||
extern NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification NS_SWIFT_NAME(deviceManagerDeviceDidDisconnect);
|
|
||||||
|
|
||||||
@interface ALTDeviceManager : NSObject
|
|
||||||
|
|
||||||
@property (class, nonatomic, readonly) ALTDeviceManager *sharedManager;
|
|
||||||
|
|
||||||
@property (nonatomic, readonly) NSArray<ALTDevice *> *connectedDevices;
|
|
||||||
@property (nonatomic, readonly) NSArray<ALTDevice *> *availableDevices;
|
|
||||||
|
|
||||||
- (void)start;
|
|
||||||
|
|
||||||
/* App Installation */
|
|
||||||
- (NSProgress *)installAppAtURL:(NSURL *)fileURL toDeviceWithUDID:(NSString *)udid activeProvisioningProfiles:(nullable NSSet<NSString *> *)activeProvisioningProfiles completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
|
||||||
- (void)removeAppForBundleIdentifier:(NSString *)bundleIdentifier fromDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
|
||||||
|
|
||||||
/* Provisioning Profiles */
|
|
||||||
- (void)installProvisioningProfiles:(NSSet<ALTProvisioningProfile *> *)provisioningProfiles toDeviceWithUDID:(NSString *)udid activeProvisioningProfiles:(nullable NSSet<NSString *> *)activeProvisioningProfiles completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
|
||||||
- (void)removeProvisioningProfilesForBundleIdentifiers:(NSSet<NSString *> *)bundleIdentifiers fromDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
|
||||||
|
|
||||||
/* Developer Disk Image */
|
|
||||||
- (void)isDeveloperDiskImageMountedForDevice:(ALTDevice *)device
|
|
||||||
completionHandler:(void (^)(BOOL isMounted, NSError *_Nullable error))completionHandler;
|
|
||||||
- (void)installDeveloperDiskImageAtURL:(NSURL *)diskURL signatureURL:(NSURL *)signatureURL toDevice:(ALTDevice *)device
|
|
||||||
completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
|
||||||
|
|
||||||
/* Apps */
|
|
||||||
- (void)fetchInstalledAppsOnDevice:(ALTDevice *)altDevice completionHandler:(void (^)(NSSet<ALTInstalledApp *> *_Nullable installedApps, NSError *_Nullable error))completionHandler;
|
|
||||||
|
|
||||||
/* Connections */
|
|
||||||
- (void)startWiredConnectionToDevice:(ALTDevice *)device completionHandler:(void (^)(ALTWiredConnection *_Nullable connection, NSError *_Nullable error))completionHandler;
|
|
||||||
- (void)startNotificationConnectionToDevice:(ALTDevice *)device completionHandler:(void (^)(ALTNotificationConnection *_Nullable connection, NSError *_Nullable error))completionHandler;
|
|
||||||
- (void)startDebugConnectionToDevice:(ALTDevice *)device completionHandler:(void (^)(ALTDebugConnection *_Nullable connection, NSError * _Nullable error))completionHandler;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
//
|
|
||||||
// ErrorDetailsViewController.swift
|
|
||||||
// AltServer
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 10/4/22.
|
|
||||||
// Copyright © 2022 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import AppKit
|
|
||||||
|
|
||||||
class ErrorDetailsViewController: NSViewController
|
|
||||||
{
|
|
||||||
var error: NSError? {
|
|
||||||
didSet {
|
|
||||||
self.update()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBOutlet private var errorCodeLabel: NSTextField!
|
|
||||||
@IBOutlet private var detailedDescriptionLabel: NSTextField!
|
|
||||||
|
|
||||||
override func viewDidLoad()
|
|
||||||
{
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
self.detailedDescriptionLabel.preferredMaxLayoutWidth = 800
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension ErrorDetailsViewController
|
|
||||||
{
|
|
||||||
func update()
|
|
||||||
{
|
|
||||||
if !self.isViewLoaded
|
|
||||||
{
|
|
||||||
self.loadView()
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let error = self.error else { return }
|
|
||||||
|
|
||||||
self.errorCodeLabel.stringValue = error.localizedErrorCode
|
|
||||||
|
|
||||||
let font = self.detailedDescriptionLabel.font ?? NSFont.systemFont(ofSize: 12)
|
|
||||||
let detailedDescription = error.formattedDetailedDescription(with: font)
|
|
||||||
self.detailedDescriptionLabel.attributedStringValue = detailedDescription
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func searchFAQ(_ sender: NSButton)
|
|
||||||
{
|
|
||||||
guard let error else { return }
|
|
||||||
|
|
||||||
let baseURL = URL(string: "https://faq.altstore.io/getting-started/error-codes")!
|
|
||||||
var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)!
|
|
||||||
|
|
||||||
let nsError = error as NSError
|
|
||||||
let query = [nsError.domain, "\(error.displayCode)"].joined(separator: "+")
|
|
||||||
components.queryItems = [URLQueryItem(name: "q", value: query)]
|
|
||||||
|
|
||||||
let url = components.url ?? baseURL
|
|
||||||
NSWorkspace.shared.open(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
//
|
|
||||||
// FileManager+URLs.swift
|
|
||||||
// AltServer
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 2/23/21.
|
|
||||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
extension FileManager
|
|
||||||
{
|
|
||||||
var altserverDirectory: URL {
|
|
||||||
let applicationSupportDirectoryURL = self.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
|
|
||||||
|
|
||||||
let altserverDirectoryURL = applicationSupportDirectoryURL.appendingPathComponent("com.rileytestut.AltServer")
|
|
||||||
return altserverDirectoryURL
|
|
||||||
}
|
|
||||||
|
|
||||||
var certificatesDirectory: URL {
|
|
||||||
let certificatesDirectoryURL = self.altserverDirectory.appendingPathComponent("Certificates")
|
|
||||||
return certificatesDirectoryURL
|
|
||||||
}
|
|
||||||
|
|
||||||
var developerDisksDirectory: URL {
|
|
||||||
let developerDisksDirectoryURL = self.altserverDirectory.appendingPathComponent("DeveloperDiskImages")
|
|
||||||
return developerDisksDirectoryURL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
//
|
|
||||||
// UserDefaults+AltServer.swift
|
|
||||||
// AltServer
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 7/31/19.
|
|
||||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
extension UserDefaults
|
|
||||||
{
|
|
||||||
var serverID: String? {
|
|
||||||
get {
|
|
||||||
return self.string(forKey: "serverID")
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
self.set(newValue, forKey: "serverID")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var didPresentInitialNotification: Bool {
|
|
||||||
get {
|
|
||||||
return self.bool(forKey: "didPresentInitialNotification")
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
self.set(newValue, forKey: "didPresentInitialNotification")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerDefaults()
|
|
||||||
{
|
|
||||||
if self.serverID == nil
|
|
||||||
{
|
|
||||||
self.serverID = UUID().uuidString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
|
||||||
<key>CFBundleExecutable</key>
|
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
|
||||||
<key>CFBundleIconFile</key>
|
|
||||||
<string></string>
|
|
||||||
<key>CFBundleIdentifier</key>
|
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
|
||||||
<string>6.0</string>
|
|
||||||
<key>CFBundleName</key>
|
|
||||||
<string>$(PRODUCT_NAME)</string>
|
|
||||||
<key>CFBundlePackageType</key>
|
|
||||||
<string>APPL</string>
|
|
||||||
<key>CFBundleShortVersionString</key>
|
|
||||||
<string>$(MARKETING_VERSION)</string>
|
|
||||||
<key>CFBundleVersion</key>
|
|
||||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
|
||||||
<key>LSMinimumSystemVersion</key>
|
|
||||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
|
||||||
<key>LSUIElement</key>
|
|
||||||
<true/>
|
|
||||||
<key>NSHumanReadableCopyright</key>
|
|
||||||
<string>Copyright © 2019 Riley Testut. All rights reserved.</string>
|
|
||||||
<key>NSMainStoryboardFile</key>
|
|
||||||
<string>Main</string>
|
|
||||||
<key>NSPrincipalClass</key>
|
|
||||||
<string>NSApplication</string>
|
|
||||||
<key>SUFeedURL</key>
|
|
||||||
<string>https://altstore.io/altserver/sparkle-macos.xml</string>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
//
|
|
||||||
// InstalledApp.swift
|
|
||||||
// AltServer
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 5/25/21.
|
|
||||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
@objc(ALTInstalledApp) @objcMembers
|
|
||||||
class InstalledApp: NSObject, MenuDisplayable
|
|
||||||
{
|
|
||||||
let name: String
|
|
||||||
let bundleIdentifier: String
|
|
||||||
let executableName: String
|
|
||||||
|
|
||||||
init?(dictionary: [String: Any])
|
|
||||||
{
|
|
||||||
guard let name = dictionary[kCFBundleNameKey as String] as? String,
|
|
||||||
let bundleIdentifier = dictionary[kCFBundleIdentifierKey as String] as? String,
|
|
||||||
let executableName = dictionary[kCFBundleExecutableKey as String] as? String
|
|
||||||
else { return nil }
|
|
||||||
|
|
||||||
self.name = name
|
|
||||||
self.bundleIdentifier = bundleIdentifier
|
|
||||||
self.executableName = executableName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
//
|
|
||||||
// MenuController.swift
|
|
||||||
// AltServer
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 3/3/21.
|
|
||||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import AppKit
|
|
||||||
|
|
||||||
protocol MenuDisplayable
|
|
||||||
{
|
|
||||||
var name: String { get }
|
|
||||||
}
|
|
||||||
|
|
||||||
class MenuController<T: MenuDisplayable & Hashable>: NSObject, NSMenuDelegate
|
|
||||||
{
|
|
||||||
let menu: NSMenu
|
|
||||||
|
|
||||||
var items: [T] {
|
|
||||||
didSet {
|
|
||||||
self.submenus.removeAll()
|
|
||||||
self.updateMenu()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var placeholder: String? {
|
|
||||||
didSet {
|
|
||||||
self.updateMenu()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var action: ((T) -> Void)?
|
|
||||||
|
|
||||||
var submenuHandler: ((T) -> NSMenu)?
|
|
||||||
private var submenus = [T: NSMenu]()
|
|
||||||
|
|
||||||
init(menu: NSMenu, items: [T])
|
|
||||||
{
|
|
||||||
self.menu = menu
|
|
||||||
self.items = items
|
|
||||||
|
|
||||||
super.init()
|
|
||||||
|
|
||||||
self.menu.delegate = self
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc
|
|
||||||
private func performAction(_ menuItem: NSMenuItem)
|
|
||||||
{
|
|
||||||
guard case let index = self.menu.index(of: menuItem), index != -1 else { return }
|
|
||||||
|
|
||||||
let item = self.items[index]
|
|
||||||
self.action?(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc
|
|
||||||
func numberOfItems(in menu: NSMenu) -> Int
|
|
||||||
{
|
|
||||||
let numberOfItems = (self.items.isEmpty && self.placeholder != nil) ? 1 : self.items.count
|
|
||||||
return numberOfItems
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc
|
|
||||||
func menu(_ menu: NSMenu, update menuItem: NSMenuItem, at index: Int, shouldCancel: Bool) -> Bool
|
|
||||||
{
|
|
||||||
if let text = self.placeholder, self.items.isEmpty
|
|
||||||
{
|
|
||||||
menuItem.title = text
|
|
||||||
menuItem.isEnabled = false
|
|
||||||
menuItem.target = nil
|
|
||||||
menuItem.action = nil
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
let item = self.items[index]
|
|
||||||
|
|
||||||
menuItem.title = item.name
|
|
||||||
menuItem.isEnabled = true
|
|
||||||
menuItem.target = self
|
|
||||||
menuItem.action = #selector(MenuController.performAction(_:))
|
|
||||||
menuItem.tag = index
|
|
||||||
|
|
||||||
if let submenu = self.submenus[item] ?? self.submenuHandler?(item)
|
|
||||||
{
|
|
||||||
menuItem.submenu = submenu
|
|
||||||
|
|
||||||
// Cache submenu to prevent duplicate calls to submenuHandler.
|
|
||||||
self.submenus[item] = submenu
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension MenuController
|
|
||||||
{
|
|
||||||
func updateMenu()
|
|
||||||
{
|
|
||||||
self.menu.removeAllItems()
|
|
||||||
|
|
||||||
let numberOfItems = self.numberOfItems(in: self.menu)
|
|
||||||
for index in 0 ..< numberOfItems
|
|
||||||
{
|
|
||||||
let menuItem = NSMenuItem(title: "", action: nil, keyEquivalent: "")
|
|
||||||
guard self.menu(self.menu, update: menuItem, at: index, shouldCancel: false) else { break }
|
|
||||||
|
|
||||||
self.menu.addItem(menuItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.menu.update()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,419 +0,0 @@
|
|||||||
//
|
|
||||||
// PluginManager.swift
|
|
||||||
// AltServer
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 9/16/20.
|
|
||||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import AppKit
|
|
||||||
import CryptoKit
|
|
||||||
|
|
||||||
import STPrivilegedTask
|
|
||||||
|
|
||||||
private let pluginDirectoryURL = URL(fileURLWithPath: "/Library/Mail/Bundles", isDirectory: true)
|
|
||||||
private let pluginURL = pluginDirectoryURL.appendingPathComponent("AltPlugin.mailbundle")
|
|
||||||
|
|
||||||
extension PluginError
|
|
||||||
{
|
|
||||||
enum Code: Int, ALTErrorCode
|
|
||||||
{
|
|
||||||
typealias Error = PluginError
|
|
||||||
|
|
||||||
case cancelled
|
|
||||||
case unknown
|
|
||||||
case notFound
|
|
||||||
case mismatchedHash
|
|
||||||
case taskError
|
|
||||||
case taskErrorCode
|
|
||||||
}
|
|
||||||
|
|
||||||
static let cancelled = PluginError(code: .cancelled)
|
|
||||||
static let notFound = PluginError(code: .notFound)
|
|
||||||
|
|
||||||
static func unknown(file: String = #fileID, line: UInt = #line) -> PluginError { PluginError(code: .unknown, sourceFile: file, sourceLine: line) }
|
|
||||||
static func mismatchedHash(hash: String, expectedHash: String) -> PluginError { PluginError(code: .mismatchedHash, hash: hash, expectedHash: expectedHash) }
|
|
||||||
static func taskError(output: String) -> PluginError { PluginError(code: .taskError, taskErrorOutput: output) }
|
|
||||||
static func taskErrorCode(_ code: Int) -> PluginError { PluginError(code: .taskErrorCode, taskErrorCode: code) }
|
|
||||||
}
|
|
||||||
|
|
||||||
struct PluginError: ALTLocalizedError
|
|
||||||
{
|
|
||||||
let code: Code
|
|
||||||
|
|
||||||
var errorTitle: String?
|
|
||||||
var errorFailure: String?
|
|
||||||
var sourceFile: String?
|
|
||||||
var sourceLine: UInt?
|
|
||||||
|
|
||||||
var hash: String?
|
|
||||||
var expectedHash: String?
|
|
||||||
var taskErrorOutput: String?
|
|
||||||
var taskErrorCode: Int?
|
|
||||||
|
|
||||||
var errorFailureReason: String {
|
|
||||||
switch self.code
|
|
||||||
{
|
|
||||||
case .cancelled: return NSLocalizedString("Mail plug-in installation was cancelled.", comment: "")
|
|
||||||
case .unknown: return NSLocalizedString("Failed to install Mail plug-in.", comment: "")
|
|
||||||
case .notFound: return NSLocalizedString("The Mail plug-in does not exist at the requested URL.", comment: "")
|
|
||||||
case .mismatchedHash:
|
|
||||||
let baseMessage = NSLocalizedString("The hash of the downloaded Mail plug-in does not match the expected hash.", comment: "")
|
|
||||||
guard let hash = self.hash, let expectedHash = self.expectedHash else { return baseMessage }
|
|
||||||
|
|
||||||
let additionalInfo = String(format: NSLocalizedString("Hash:\n%@\n\nExpected Hash:\n%@", comment: ""), hash, expectedHash)
|
|
||||||
return baseMessage + "\n\n" + additionalInfo
|
|
||||||
|
|
||||||
case .taskError:
|
|
||||||
if let output = self.taskErrorOutput
|
|
||||||
{
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use .taskErrorCode base message as fallback.
|
|
||||||
fallthrough
|
|
||||||
|
|
||||||
case .taskErrorCode:
|
|
||||||
let baseMessage = NSLocalizedString("There was an error installing the Mail plug-in.", comment: "")
|
|
||||||
guard let errorCode = self.taskErrorCode else { return baseMessage }
|
|
||||||
|
|
||||||
let additionalInfo = String(format: NSLocalizedString("(Error Code: %@)", comment: ""), NSNumber(value: errorCode))
|
|
||||||
return baseMessage + " " + additionalInfo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension URL
|
|
||||||
{
|
|
||||||
#if STAGING
|
|
||||||
static let altPluginUpdateURL = URL(string: "https://f000.backblazeb2.com/file/altstore-staging/altserver/altplugin/altplugin.json")!
|
|
||||||
#else
|
|
||||||
static let altPluginUpdateURL = URL(string: "https://cdn.altstore.io/file/altstore/altserver/altplugin/altplugin.json")!
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
class PluginManager
|
|
||||||
{
|
|
||||||
private let session = URLSession(configuration: .ephemeral)
|
|
||||||
private var latestPluginVersion: PluginVersion?
|
|
||||||
|
|
||||||
var isMailPluginInstalled: Bool {
|
|
||||||
let isMailPluginInstalled = FileManager.default.fileExists(atPath: pluginURL.path)
|
|
||||||
return isMailPluginInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
func isUpdateAvailable(completionHandler: @escaping (Result<Bool, Error>) -> Void)
|
|
||||||
{
|
|
||||||
self.isUpdateAvailable(useCache: false, completionHandler: completionHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func isUpdateAvailable(useCache: Bool, completionHandler: @escaping (Result<Bool, Error>) -> Void)
|
|
||||||
{
|
|
||||||
do
|
|
||||||
{
|
|
||||||
// If Mail plug-in is not yet installed, then there is no update available.
|
|
||||||
var isDirectory: ObjCBool = false
|
|
||||||
guard FileManager.default.fileExists(atPath: pluginURL.path, isDirectory: &isDirectory), isDirectory.boolValue else { return completionHandler(.success(false)) }
|
|
||||||
|
|
||||||
// Load Info.plist from disk because Bundle.infoDictionary is cached by system.
|
|
||||||
let infoDictionaryURL = pluginURL.appendingPathComponent("Contents/Info.plist")
|
|
||||||
guard let infoDictionary = NSDictionary(contentsOf: infoDictionaryURL) as? [String: Any],
|
|
||||||
let localVersion = infoDictionary["CFBundleShortVersionString"] as? String
|
|
||||||
else { throw CocoaError(.fileReadCorruptFile, userInfo: [NSURLErrorKey: infoDictionaryURL]) }
|
|
||||||
|
|
||||||
if let pluginVersion = self.latestPluginVersion, useCache
|
|
||||||
{
|
|
||||||
let isUpdateAvailable = (localVersion != pluginVersion.version)
|
|
||||||
completionHandler(.success(isUpdateAvailable))
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
self.fetchLatestPluginVersion(useCache: useCache) { result in
|
|
||||||
switch result
|
|
||||||
{
|
|
||||||
case .failure(let error): completionHandler(.failure(error))
|
|
||||||
case .success(let pluginVersion):
|
|
||||||
let isUpdateAvailable = (localVersion != pluginVersion.version)
|
|
||||||
completionHandler(.success(isUpdateAvailable))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
completionHandler(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension PluginManager
|
|
||||||
{
|
|
||||||
func installMailPlugin(completionHandler: @escaping (Result<Void, Error>) -> Void)
|
|
||||||
{
|
|
||||||
self.isUpdateAvailable(useCache: true) { result in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
do
|
|
||||||
{
|
|
||||||
let isUpdateAvailable = try result.get()
|
|
||||||
|
|
||||||
let alert = NSAlert()
|
|
||||||
if isUpdateAvailable
|
|
||||||
{
|
|
||||||
alert.messageText = NSLocalizedString("Update Mail Plug-in", comment: "")
|
|
||||||
alert.informativeText = NSLocalizedString("An update is available for AltServer's Mail plug-in. Please update the plug-in now in order to keep using AltStore.", comment: "")
|
|
||||||
|
|
||||||
alert.addButton(withTitle: NSLocalizedString("Update Plug-in", comment: ""))
|
|
||||||
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
alert.messageText = NSLocalizedString("Install Mail Plug-in", comment: "")
|
|
||||||
alert.informativeText = NSLocalizedString("AltServer requires a Mail plug-in in order to retrieve necessary information about your Apple ID. Would you like to install it now?", comment: "")
|
|
||||||
|
|
||||||
alert.addButton(withTitle: NSLocalizedString("Install Plug-in", comment: ""))
|
|
||||||
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
|
|
||||||
}
|
|
||||||
|
|
||||||
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
|
|
||||||
|
|
||||||
let response = alert.runModal()
|
|
||||||
guard response == .alertFirstButtonReturn else { throw PluginError.cancelled }
|
|
||||||
|
|
||||||
self.downloadPlugin { (result) in
|
|
||||||
do
|
|
||||||
{
|
|
||||||
let fileURL = try result.get()
|
|
||||||
|
|
||||||
// Ensure plug-in directory exists.
|
|
||||||
let authorization = try self.runAndKeepAuthorization("mkdir", arguments: ["-p", pluginDirectoryURL.path])
|
|
||||||
|
|
||||||
// Create temporary directory.
|
|
||||||
let temporaryDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
||||||
try FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
|
||||||
defer { try? FileManager.default.removeItem(at: temporaryDirectoryURL) }
|
|
||||||
|
|
||||||
// Unzip AltPlugin to temporary directory.
|
|
||||||
try self.runAndKeepAuthorization("unzip", arguments: ["-o", fileURL.path, "-d", temporaryDirectoryURL.path], authorization: authorization)
|
|
||||||
|
|
||||||
if FileManager.default.fileExists(atPath: pluginURL.path)
|
|
||||||
{
|
|
||||||
// Delete existing Mail plug-in.
|
|
||||||
try self.runAndKeepAuthorization("rm", arguments: ["-rf", pluginURL.path], authorization: authorization)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy AltPlugin to Mail plug-ins directory.
|
|
||||||
// Must be separate step than unzip to prevent macOS from considering plug-in corrupted.
|
|
||||||
let unzippedPluginURL = temporaryDirectoryURL.appendingPathComponent(pluginURL.lastPathComponent)
|
|
||||||
try self.runAndKeepAuthorization("cp", arguments: ["-R", unzippedPluginURL.path, pluginDirectoryURL.path], authorization: authorization)
|
|
||||||
|
|
||||||
guard self.isMailPluginInstalled else { throw PluginError.unknown() }
|
|
||||||
|
|
||||||
// Enable Mail plug-in preferences.
|
|
||||||
try self.run("defaults", arguments: ["write", "/Library/Preferences/com.apple.mail", "EnableBundles", "-bool", "YES"], authorization: authorization)
|
|
||||||
|
|
||||||
print("Finished installing Mail plug-in!")
|
|
||||||
|
|
||||||
completionHandler(.success(()))
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
completionHandler(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
completionHandler(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func uninstallMailPlugin(completionHandler: @escaping (Result<Void, Error>) -> Void)
|
|
||||||
{
|
|
||||||
let alert = NSAlert()
|
|
||||||
alert.messageText = NSLocalizedString("Uninstall Mail Plug-in", comment: "")
|
|
||||||
alert.informativeText = NSLocalizedString("Are you sure you want to uninstall the AltServer Mail plug-in? You will no longer be able to install or refresh apps with AltStore.", comment: "")
|
|
||||||
|
|
||||||
alert.addButton(withTitle: NSLocalizedString("Uninstall Plug-in", comment: ""))
|
|
||||||
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
|
|
||||||
|
|
||||||
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
|
|
||||||
|
|
||||||
let response = alert.runModal()
|
|
||||||
guard response == .alertFirstButtonReturn else { return completionHandler(.failure(PluginError.cancelled)) }
|
|
||||||
|
|
||||||
DispatchQueue.global().async {
|
|
||||||
do
|
|
||||||
{
|
|
||||||
if FileManager.default.fileExists(atPath: pluginURL.path)
|
|
||||||
{
|
|
||||||
// Delete Mail plug-in from privileged directory.
|
|
||||||
try self.run("rm", arguments: ["-rf", pluginURL.path])
|
|
||||||
}
|
|
||||||
|
|
||||||
completionHandler(.success(()))
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
completionHandler(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension PluginManager
|
|
||||||
{
|
|
||||||
func fetchLatestPluginVersion(useCache: Bool, completionHandler: @escaping (Result<PluginVersion, Error>) -> Void)
|
|
||||||
{
|
|
||||||
if let pluginVersion = self.latestPluginVersion, useCache
|
|
||||||
{
|
|
||||||
return completionHandler(.success(pluginVersion))
|
|
||||||
}
|
|
||||||
|
|
||||||
guard #available(macOS 11, *) else {
|
|
||||||
// macOS versions prior to 11.0 require Mail plug-ins be *unsigned*,
|
|
||||||
// so we hardcode these versions to use the unsigned AltPlugin v1.0.
|
|
||||||
return completionHandler(.success(.v1_0))
|
|
||||||
}
|
|
||||||
|
|
||||||
let dataTask = self.session.dataTask(with: .altPluginUpdateURL) { (data, response, error) in
|
|
||||||
do
|
|
||||||
{
|
|
||||||
if let response = response as? HTTPURLResponse
|
|
||||||
{
|
|
||||||
guard response.statusCode != 404 else { return completionHandler(.failure(PluginError.notFound)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let data = data else { throw error! }
|
|
||||||
|
|
||||||
let response = try JSONDecoder().decode(PluginVersionResponse.self, from: data)
|
|
||||||
completionHandler(.success(response.pluginVersion))
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
completionHandler(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dataTask.resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
func downloadPlugin(completion: @escaping (Result<URL, Error>) -> Void)
|
|
||||||
{
|
|
||||||
self.fetchLatestPluginVersion(useCache: true) { result in
|
|
||||||
switch result
|
|
||||||
{
|
|
||||||
case .failure(let error): completion(.failure(error))
|
|
||||||
case .success(let pluginVersion):
|
|
||||||
|
|
||||||
func finish(_ result: Result<URL, Error>)
|
|
||||||
{
|
|
||||||
do
|
|
||||||
{
|
|
||||||
let fileURL = try result.get()
|
|
||||||
|
|
||||||
if #available(OSX 10.15, *)
|
|
||||||
{
|
|
||||||
let data = try Data(contentsOf: fileURL)
|
|
||||||
let sha256Hash = SHA256.hash(data: data)
|
|
||||||
let hashString = sha256Hash.compactMap { String(format: "%02x", $0) }.joined()
|
|
||||||
|
|
||||||
print("Comparing Mail plug-in hash (\(hashString)) against expected hash (\(pluginVersion.sha256Hash))...")
|
|
||||||
guard hashString == pluginVersion.sha256Hash else { throw PluginError.mismatchedHash(hash: hashString, expectedHash: pluginVersion.sha256Hash) }
|
|
||||||
}
|
|
||||||
|
|
||||||
completion(.success(fileURL))
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if pluginVersion.url.isFileURL
|
|
||||||
{
|
|
||||||
finish(.success(pluginVersion.url))
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
let downloadTask = URLSession.shared.downloadTask(with: pluginVersion.url) { (fileURL, response, error) in
|
|
||||||
if let response = response as? HTTPURLResponse
|
|
||||||
{
|
|
||||||
guard response.statusCode != 404 else { return finish(.failure(PluginError.notFound)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = Result(fileURL, error)
|
|
||||||
finish(result)
|
|
||||||
|
|
||||||
if let fileURL = fileURL
|
|
||||||
{
|
|
||||||
try? FileManager.default.removeItem(at: fileURL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadTask.resume()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func run(_ program: String, arguments: [String], authorization: AuthorizationRef? = nil) throws
|
|
||||||
{
|
|
||||||
_ = try self._run(program, arguments: arguments, authorization: authorization, freeAuthorization: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
func runAndKeepAuthorization(_ program: String, arguments: [String], authorization: AuthorizationRef? = nil) throws -> AuthorizationRef
|
|
||||||
{
|
|
||||||
return try self._run(program, arguments: arguments, authorization: authorization, freeAuthorization: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _run(_ program: String, arguments: [String], authorization: AuthorizationRef? = nil, freeAuthorization: Bool) throws -> AuthorizationRef
|
|
||||||
{
|
|
||||||
var launchPath = "/usr/bin/" + program
|
|
||||||
if !FileManager.default.fileExists(atPath: launchPath)
|
|
||||||
{
|
|
||||||
launchPath = "/bin/" + program
|
|
||||||
}
|
|
||||||
|
|
||||||
print("Running program:", launchPath)
|
|
||||||
|
|
||||||
let task = STPrivilegedTask()
|
|
||||||
task.launchPath = launchPath
|
|
||||||
task.arguments = arguments
|
|
||||||
task.freeAuthorizationWhenDone = freeAuthorization
|
|
||||||
|
|
||||||
let errorCode: OSStatus
|
|
||||||
|
|
||||||
if let authorization = authorization
|
|
||||||
{
|
|
||||||
errorCode = task.launch(withAuthorization: authorization)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
errorCode = task.launch()
|
|
||||||
}
|
|
||||||
|
|
||||||
guard errorCode == 0 else { throw PluginError.taskErrorCode(Int(errorCode)) }
|
|
||||||
|
|
||||||
task.waitUntilExit()
|
|
||||||
|
|
||||||
print("Exit code:", task.terminationStatus)
|
|
||||||
|
|
||||||
guard task.terminationStatus == 0 else {
|
|
||||||
let outputData = task.outputFileHandle.readDataToEndOfFile()
|
|
||||||
|
|
||||||
if let outputString = String(data: outputData, encoding: .utf8), !outputString.isEmpty
|
|
||||||
{
|
|
||||||
throw PluginError.taskError(output: outputString)
|
|
||||||
}
|
|
||||||
|
|
||||||
throw PluginError.taskErrorCode(Int(task.terminationStatus))
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let authorization = task.authorization else { throw PluginError.unknown() }
|
|
||||||
return authorization
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
//
|
|
||||||
// PluginVersion.swift
|
|
||||||
// AltServer
|
|
||||||
//
|
|
||||||
// Created by Riley Testut and Weedles on 2/15/22 <3
|
|
||||||
// Copyright © 2022 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
struct PluginVersion: Decodable
|
|
||||||
{
|
|
||||||
var url: URL
|
|
||||||
var sha256Hash: String
|
|
||||||
var version: String
|
|
||||||
|
|
||||||
static let v1_0 = PluginVersion(url: URL(string: "https://f000.backblazeb2.com/file/altstore/altserver/altplugin/1_0.zip")!,
|
|
||||||
sha256Hash: "070e9b7e1f74e7a6474d36253ab5a3623ff93892acc9e1043c3581f2ded12200",
|
|
||||||
version: "1.0")
|
|
||||||
|
|
||||||
static let v1_9 = PluginVersion(url: Bundle.main.url(forResource: "AltPlugin", withExtension: "zip")!,
|
|
||||||
sha256Hash: "83ead26d8776ef6850e06fe3d1c5c5559aca284718b1cf3cc49785ba6b1e2849",
|
|
||||||
version: "1.9")
|
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey
|
|
||||||
{
|
|
||||||
case url
|
|
||||||
case sha256Hash = "sha256"
|
|
||||||
case version
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct PluginVersionResponse: Decodable
|
|
||||||
{
|
|
||||||
var version: Int
|
|
||||||
var pluginVersion: PluginVersion
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,6 @@
|
|||||||
<Workspace
|
<Workspace
|
||||||
version = "1.0">
|
version = "1.0">
|
||||||
<FileRef
|
<FileRef
|
||||||
location = "self:AltStore.xcodeproj">
|
location = "self:">
|
||||||
</FileRef>
|
</FileRef>
|
||||||
</Workspace>
|
</Workspace>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1020"
|
LastUpgradeVersion = "1610"
|
||||||
version = "1.3">
|
version = "1.7">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
buildImplicitDependencies = "YES">
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
<BuildActionEntries>
|
<BuildActionEntries>
|
||||||
<BuildActionEntry
|
<BuildActionEntry
|
||||||
buildForTesting = "YES"
|
buildForTesting = "YES"
|
||||||
@@ -14,9 +15,9 @@
|
|||||||
buildForAnalyzing = "YES">
|
buildForAnalyzing = "YES">
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
BlueprintIdentifier = "BF58047A246A28F7008AE704"
|
||||||
BuildableName = "AltServer.app"
|
BuildableName = "AltBackup.app"
|
||||||
BlueprintName = "AltServer"
|
BlueprintName = "AltBackup"
|
||||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildActionEntry>
|
</BuildActionEntry>
|
||||||
@@ -26,20 +27,8 @@
|
|||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
<Testables>
|
shouldAutocreateTestPlan = "YES">
|
||||||
</Testables>
|
|
||||||
<MacroExpansion>
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
|
||||||
BuildableName = "AltServer.app"
|
|
||||||
BlueprintName = "AltServer"
|
|
||||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</MacroExpansion>
|
|
||||||
<AdditionalOptions>
|
|
||||||
</AdditionalOptions>
|
|
||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
@@ -55,14 +44,42 @@
|
|||||||
runnableDebuggingMode = "0">
|
runnableDebuggingMode = "0">
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
BlueprintIdentifier = "BF58047A246A28F7008AE704"
|
||||||
BuildableName = "AltServer.app"
|
BuildableName = "AltBackup.app"
|
||||||
BlueprintName = "AltServer"
|
BlueprintName = "AltBackup"
|
||||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
<AdditionalOptions>
|
<CommandLineArguments>
|
||||||
</AdditionalOptions>
|
<CommandLineArgument
|
||||||
|
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
|
||||||
|
isEnabled = "NO">
|
||||||
|
</CommandLineArgument>
|
||||||
|
<CommandLineArgument
|
||||||
|
argument = "-com.apple.CoreData.MigrationDebug 1"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</CommandLineArgument>
|
||||||
|
<CommandLineArgument
|
||||||
|
argument = "-com.apple.CoreData.SQLiteIntegrityCheck 1"
|
||||||
|
isEnabled = "NO">
|
||||||
|
</CommandLineArgument>
|
||||||
|
<CommandLineArgument
|
||||||
|
argument = "-com.apple.CoreData.SQLDebug 1"
|
||||||
|
isEnabled = "NO">
|
||||||
|
</CommandLineArgument>
|
||||||
|
</CommandLineArguments>
|
||||||
|
<EnvironmentVariables>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "OS_ACTIVITY_MODE"
|
||||||
|
value = "$(DEBUG_ACTIVITY_MODE)"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "OBJC_DEBUG_DUPLICATE_CLASSES"
|
||||||
|
value = "$(DEBUG_DUPLICATE_CLASSES)"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
</EnvironmentVariables>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction
|
<ProfileAction
|
||||||
buildConfiguration = "Release"
|
buildConfiguration = "Release"
|
||||||
@@ -74,9 +91,9 @@
|
|||||||
runnableDebuggingMode = "0">
|
runnableDebuggingMode = "0">
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
BlueprintIdentifier = "BF58047A246A28F7008AE704"
|
||||||
BuildableName = "AltServer.app"
|
BuildableName = "AltBackup.app"
|
||||||
BlueprintName = "AltServer"
|
BlueprintName = "AltBackup"
|
||||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1120"
|
LastUpgradeVersion = "2620"
|
||||||
version = "1.3">
|
version = "1.7">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
buildImplicitDependencies = "YES">
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
<BuildActionEntries>
|
<BuildActionEntries>
|
||||||
<BuildActionEntry
|
<BuildActionEntry
|
||||||
buildForTesting = "YES"
|
buildForTesting = "YES"
|
||||||
@@ -14,9 +15,9 @@
|
|||||||
buildForAnalyzing = "YES">
|
buildForAnalyzing = "YES">
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "BF5C5FC4237DF5AE00EDD0C6"
|
BlueprintIdentifier = "BF66EE7D2501AE50007EE018"
|
||||||
BuildableName = "AltPlugin.mailbundle"
|
BuildableName = "AltStoreCore.framework"
|
||||||
BlueprintName = "AltPlugin"
|
BlueprintName = "AltStoreCore"
|
||||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildActionEntry>
|
</BuildActionEntry>
|
||||||
@@ -26,15 +27,14 @@
|
|||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
<Testables>
|
shouldAutocreateTestPlan = "YES">
|
||||||
</Testables>
|
|
||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
launchStyle = "1"
|
launchStyle = "0"
|
||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
ignoresPersistentStateOnLaunch = "NO"
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
debugDocumentVersioning = "YES"
|
debugDocumentVersioning = "YES"
|
||||||
@@ -50,9 +50,9 @@
|
|||||||
<MacroExpansion>
|
<MacroExpansion>
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "BF5C5FC4237DF5AE00EDD0C6"
|
BlueprintIdentifier = "BF66EE7D2501AE50007EE018"
|
||||||
BuildableName = "AltPlugin.mailbundle"
|
BuildableName = "AltStoreCore.framework"
|
||||||
BlueprintName = "AltPlugin"
|
BlueprintName = "AltStoreCore"
|
||||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</MacroExpansion>
|
</MacroExpansion>
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1150"
|
LastUpgradeVersion = "2620"
|
||||||
version = "1.3">
|
wasCreatedForAppExtension = "YES"
|
||||||
|
version = "2.0">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "NO"
|
parallelizeBuildables = "YES"
|
||||||
buildImplicitDependencies = "YES">
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
<BuildActionEntries>
|
<BuildActionEntries>
|
||||||
<BuildActionEntry
|
<BuildActionEntry
|
||||||
buildForTesting = "YES"
|
buildForTesting = "YES"
|
||||||
@@ -14,23 +16,9 @@
|
|||||||
buildForAnalyzing = "YES">
|
buildForAnalyzing = "YES">
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "A7A6DC28A6D60809855FE404C6A3EA29"
|
BlueprintIdentifier = "BF989166250AABF3002ACF50"
|
||||||
BuildableName = "libPods-AltDaemon.a"
|
BuildableName = "AltWidgetExtension.appex"
|
||||||
BlueprintName = "Pods-AltDaemon"
|
BlueprintName = "AltWidgetExtension"
|
||||||
ReferencedContainer = "container:Pods/Pods.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildActionEntry>
|
|
||||||
<BuildActionEntry
|
|
||||||
buildForTesting = "YES"
|
|
||||||
buildForRunning = "YES"
|
|
||||||
buildForProfiling = "YES"
|
|
||||||
buildForArchiving = "YES"
|
|
||||||
buildForAnalyzing = "YES">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "BF1E314F22A0616100370A3C"
|
|
||||||
BuildableName = "libAltKit.a"
|
|
||||||
BlueprintName = "AltKit"
|
|
||||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildActionEntry>
|
</BuildActionEntry>
|
||||||
@@ -42,9 +30,9 @@
|
|||||||
buildForAnalyzing = "YES">
|
buildForAnalyzing = "YES">
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
|
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||||
BuildableName = "AltDaemon"
|
BuildableName = "SideStore.app"
|
||||||
BlueprintName = "AltDaemon"
|
BlueprintName = "SideStore"
|
||||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildActionEntry>
|
</BuildActionEntry>
|
||||||
@@ -54,33 +42,45 @@
|
|||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
<Testables>
|
shouldAutocreateTestPlan = "YES">
|
||||||
</Testables>
|
|
||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
buildConfiguration = "Release"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = ""
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||||
launchStyle = "0"
|
launchStyle = "0"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
ignoresPersistentStateOnLaunch = "NO"
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
debugDocumentVersioning = "YES"
|
debugDocumentVersioning = "YES"
|
||||||
debugServiceExtension = "internal"
|
debugServiceExtension = "internal"
|
||||||
allowLocationSimulation = "YES">
|
allowLocationSimulation = "YES"
|
||||||
<MacroExpansion>
|
launchAutomaticallySubstyle = "2">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
|
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||||
BuildableName = "AltDaemon"
|
BuildableName = "SideStore.app"
|
||||||
BlueprintName = "AltDaemon"
|
BlueprintName = "SideStore"
|
||||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</MacroExpansion>
|
</BuildableProductRunnable>
|
||||||
<EnvironmentVariables>
|
<EnvironmentVariables>
|
||||||
<EnvironmentVariable
|
<EnvironmentVariable
|
||||||
key = "THEOS"
|
key = "_XCWidgetKind"
|
||||||
value = "~/theos"
|
value = ""
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "_XCWidgetDefaultView"
|
||||||
|
value = "timeline"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "_XCWidgetFamily"
|
||||||
|
value = "systemMedium"
|
||||||
isEnabled = "YES">
|
isEnabled = "YES">
|
||||||
</EnvironmentVariable>
|
</EnvironmentVariable>
|
||||||
</EnvironmentVariables>
|
</EnvironmentVariables>
|
||||||
@@ -90,16 +90,19 @@
|
|||||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
savedToolIdentifier = ""
|
savedToolIdentifier = ""
|
||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
debugDocumentVersioning = "YES">
|
debugDocumentVersioning = "YES"
|
||||||
<MacroExpansion>
|
askForAppToLaunch = "Yes"
|
||||||
|
launchAutomaticallySubstyle = "2">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
|
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||||
BuildableName = "AltDaemon"
|
BuildableName = "SideStore.app"
|
||||||
BlueprintName = "AltDaemon"
|
BlueprintName = "SideStore"
|
||||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</MacroExpansion>
|
</BuildableProductRunnable>
|
||||||
</ProfileAction>
|
</ProfileAction>
|
||||||
<AnalyzeAction
|
<AnalyzeAction
|
||||||
buildConfiguration = "Debug">
|
buildConfiguration = "Debug">
|
||||||
@@ -1,36 +1,32 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1400"
|
LastUpgradeVersion = "1620"
|
||||||
version = "1.3">
|
version = "1.7">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
buildImplicitDependencies = "YES">
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
</BuildAction>
|
</BuildAction>
|
||||||
<TestAction
|
<TestAction
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
codeCoverageEnabled = "YES"
|
<TestPlans>
|
||||||
onlyGenerateCoverageForSpecifiedTargets = "YES">
|
<TestPlanReference
|
||||||
<CodeCoverageTargets>
|
reference = "container:SideStore/Tests/DataStructureTests.xctestplan"
|
||||||
<BuildableReference
|
default = "YES">
|
||||||
BuildableIdentifier = "primary"
|
</TestPlanReference>
|
||||||
BlueprintIdentifier = "BF66EE7D2501AE50007EE018"
|
</TestPlans>
|
||||||
BuildableName = "AltStoreCore.framework"
|
|
||||||
BlueprintName = "AltStoreCore"
|
|
||||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</CodeCoverageTargets>
|
|
||||||
<Testables>
|
<Testables>
|
||||||
<TestableReference
|
<TestableReference
|
||||||
skipped = "NO"
|
skipped = "NO"
|
||||||
parallelizable = "YES">
|
parallelizable = "YES">
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "D586D39728EF58B0000E101F"
|
BlueprintIdentifier = "A81A8CC42D68BA610086C96F"
|
||||||
BuildableName = "AltTests.xctest"
|
BuildableName = "DataStructureTests.xctest"
|
||||||
BlueprintName = "AltTests"
|
BlueprintName = "DataStructureTests"
|
||||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</TestableReference>
|
</TestableReference>
|
||||||