mirror of
https://github.com/tauri-apps/tauri.git
synced 2026-04-11 10:43:31 +02:00
Compare commits
904 Commits
@tauri-app
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
926a57bb08 | ||
|
|
074299c08d | ||
|
|
ec5381e951 | ||
|
|
b27be063ff | ||
|
|
cdf5276478 | ||
|
|
e5b00795c2 | ||
|
|
5a0ca7edbb | ||
|
|
5dc2cee603 | ||
|
|
3b5b2cc125 | ||
|
|
e032c3b342 | ||
|
|
386312c73a | ||
|
|
d34497ef15 | ||
|
|
4017a7ed73 | ||
|
|
093e2b47c0 | ||
|
|
f0381b4bdf | ||
|
|
d730770bb9 | ||
|
|
80c1425af8 | ||
|
|
1ef6a119b1 | ||
|
|
aabb42f949 | ||
|
|
fcb702ec4d | ||
|
|
f17240bf6c | ||
|
|
1fa1db5cd9 | ||
|
|
8db451c791 | ||
|
|
15b311155f | ||
|
|
812b2990e7 | ||
|
|
c8d7003b23 | ||
|
|
2679ddd5eb | ||
|
|
3f77cc1ee6 | ||
|
|
2dd9b15a2b | ||
|
|
eacd36a4ea | ||
|
|
6cb86c9e42 | ||
|
|
50833703e4 | ||
|
|
8718d08163 | ||
|
|
8230973ae8 | ||
|
|
9b17a7aeae | ||
|
|
d86827980d | ||
|
|
3a65cc6885 | ||
|
|
52cf195b78 | ||
|
|
c3cbff3f74 | ||
|
|
33754ae5e3 | ||
|
|
3935dee121 | ||
|
|
33932a72b2 | ||
|
|
7d3c7593a9 | ||
|
|
f20256bca5 | ||
|
|
03514414d9 | ||
|
|
7b16dafb1d | ||
|
|
7782c5525a | ||
|
|
aa0bf8bd19 | ||
|
|
91fb0e161f | ||
|
|
88c05689c8 | ||
|
|
6252432f07 | ||
|
|
2d0b80eb1c | ||
|
|
86c8c870c8 | ||
|
|
476e8ee7f3 | ||
|
|
0d1cb83bab | ||
|
|
35c35f27ae | ||
|
|
7d01aa0417 | ||
|
|
7be58a1c64 | ||
|
|
06374a902a | ||
|
|
c37368f339 | ||
|
|
06f911aaff | ||
|
|
eb5d88427a | ||
|
|
540c5b4e59 | ||
|
|
5dbb37bab1 | ||
|
|
19ded696de | ||
|
|
08558b8ba4 | ||
|
|
ce8fddb464 | ||
|
|
517b81e970 | ||
|
|
cd68b03ee5 | ||
|
|
8d67af37b6 | ||
|
|
9f0306fbcc | ||
|
|
f7c083cd41 | ||
|
|
32576120fd | ||
|
|
e3fdcb5002 | ||
|
|
d453e2e06a | ||
|
|
20b99f9281 | ||
|
|
3a4e165b6f | ||
|
|
efc4c26ebc | ||
|
|
7fca58230f | ||
|
|
c769f211fc | ||
|
|
4d5d78daf6 | ||
|
|
4794a6ba22 | ||
|
|
09a4e7f55a | ||
|
|
c862a0bd8c | ||
|
|
f82594410c | ||
|
|
853ed4642f | ||
|
|
53611c4d7b | ||
|
|
62aa13a124 | ||
|
|
e919a760ed | ||
|
|
0575dd287e | ||
|
|
eccff97588 | ||
|
|
08e35fcda0 | ||
|
|
10a8066db3 | ||
|
|
ea31b07f19 | ||
|
|
7f7d9aac21 | ||
|
|
7873c4a1c6 | ||
|
|
123d63a0c1 | ||
|
|
75057c7c08 | ||
|
|
268bb339f0 | ||
|
|
07788af13f | ||
|
|
9a53c84ec0 | ||
|
|
137576e8a4 | ||
|
|
1b0e335d3f | ||
|
|
84b04c4a8d | ||
|
|
897529d7a2 | ||
|
|
3d102e0c13 | ||
|
|
fea4d02403 | ||
|
|
a03219ca19 | ||
|
|
b75ea5bead | ||
|
|
dcd1a65889 | ||
|
|
9b242e40c8 | ||
|
|
1dbf6fd067 | ||
|
|
8a43e4f9d9 | ||
|
|
a2abe2e6bc | ||
|
|
51f0fcb69c | ||
|
|
0650852d14 | ||
|
|
c1d82eb3a3 | ||
|
|
51a0d6d66d | ||
|
|
7f48ee9068 | ||
|
|
e290642fb4 | ||
|
|
b79386010d | ||
|
|
ff5d76ca21 | ||
|
|
2d28e3143e | ||
|
|
18c69df8c7 | ||
|
|
f2e0405dc2 | ||
|
|
54e8d93db1 | ||
|
|
251203b896 | ||
|
|
91becd9e4f | ||
|
|
018b4db22e | ||
|
|
731dd5bfdc | ||
|
|
7b1b3514df | ||
|
|
546b296405 | ||
|
|
514cf21e14 | ||
|
|
60174527c0 | ||
|
|
4176f93ae4 | ||
|
|
4408f72af6 | ||
|
|
1496145f82 | ||
|
|
f022b2d1ae | ||
|
|
1573c72402 | ||
|
|
dd7e59a495 | ||
|
|
2d2a1be429 | ||
|
|
afdd288eab | ||
|
|
79a7d9ec01 | ||
|
|
f855caf8a3 | ||
|
|
ee3cc4a91b | ||
|
|
b5ef603d84 | ||
|
|
ce98d87ce0 | ||
|
|
ad1dec2e24 | ||
|
|
beffcd880f | ||
|
|
956031d73d | ||
|
|
4b00130b86 | ||
|
|
8e3bd63db9 | ||
|
|
cfe47871a5 | ||
|
|
236f55b7aa | ||
|
|
9bb7e79e97 | ||
|
|
d566679a99 | ||
|
|
3899d456d4 | ||
|
|
b586ecf1f4 | ||
|
|
dd70d213cd | ||
|
|
d06a1994e9 | ||
|
|
b446a858de | ||
|
|
85ba5315c2 | ||
|
|
779612ac84 | ||
|
|
22edc65aad | ||
|
|
9a19226369 | ||
|
|
fd8c30b4f1 | ||
|
|
18464d9481 | ||
|
|
b80f9deb5f | ||
|
|
1afa9df6d5 | ||
|
|
75a1fec705 | ||
|
|
100dc94c48 | ||
|
|
7f710b8f3b | ||
|
|
bda1d22369 | ||
|
|
28b9e7c7b8 | ||
|
|
3056d44d96 | ||
|
|
fc017ee257 | ||
|
|
67c7418c06 | ||
|
|
f59bf9d539 | ||
|
|
4b6b8690ab | ||
|
|
cdc5594286 | ||
|
|
a1c231ec29 | ||
|
|
752c923002 | ||
|
|
cb28f4368c | ||
|
|
6aa7f2d852 | ||
|
|
06f26bbb24 | ||
|
|
68cb318979 | ||
|
|
3397fd9bfe | ||
|
|
3b4fac2017 | ||
|
|
684791efa6 | ||
|
|
25e920e169 | ||
|
|
a279485856 | ||
|
|
7b0d4e7322 | ||
|
|
c5008b829d | ||
|
|
b5aa018702 | ||
|
|
55453e8453 | ||
|
|
75082cc5b3 | ||
|
|
006d592837 | ||
|
|
d2938486e9 | ||
|
|
19fb6f7cb0 | ||
|
|
3d6868d09c | ||
|
|
cc8c0b5317 | ||
|
|
20e53a4b95 | ||
|
|
08bda64c25 | ||
|
|
28a2f9bc55 | ||
|
|
ed7c9a4100 | ||
|
|
abf7e8850b | ||
|
|
b0012424c5 | ||
|
|
06d4a4ed6c | ||
|
|
a99601ee4b | ||
|
|
2e089f6acb | ||
|
|
6bbb530fd5 | ||
|
|
b06b3bd091 | ||
|
|
eb60b9966b | ||
|
|
94cbd40fc7 | ||
|
|
673867aa0e | ||
|
|
4188ffdafc | ||
|
|
12a6787110 | ||
|
|
6cb73194c4 | ||
|
|
d1892b97ce | ||
|
|
e446926a6a | ||
|
|
b0c493a4ea | ||
|
|
d340b8c8b1 | ||
|
|
830146d0be | ||
|
|
fa3771b7bc | ||
|
|
9efe474e06 | ||
|
|
69476d8e23 | ||
|
|
f5851ee00d | ||
|
|
66cb1dbbef | ||
|
|
a58d461eb0 | ||
|
|
2a06d10066 | ||
|
|
59089723fc | ||
|
|
1a6627ee7d | ||
|
|
f6622a3e34 | ||
|
|
80eadb7387 | ||
|
|
346a420812 | ||
|
|
5239d39149 | ||
|
|
0b1da30d28 | ||
|
|
7db7142f9f | ||
|
|
a9b342125d | ||
|
|
bcf000c0a8 | ||
|
|
61b9b681e8 | ||
|
|
c37a298331 | ||
|
|
b8b866fcc7 | ||
|
|
956b4fd6ff | ||
|
|
07e134f70e | ||
|
|
f70b28529d | ||
|
|
c23bec62d6 | ||
|
|
9a35a616f5 | ||
|
|
755eb33d1c | ||
|
|
df61fac2b5 | ||
|
|
16348ac2bd | ||
|
|
03e7c11932 | ||
|
|
e81635aa3d | ||
|
|
0ac89d3b6c | ||
|
|
4791d09a0a | ||
|
|
bc829ee24d | ||
|
|
11800a0071 | ||
|
|
662b39adb3 | ||
|
|
2aaa801c35 | ||
|
|
5349984064 | ||
|
|
5f535b4150 | ||
|
|
f3df96fb38 | ||
|
|
c0d3f9d47e | ||
|
|
d54f3b95a6 | ||
|
|
1e7aac355f | ||
|
|
8d869717da | ||
|
|
f0172a454a | ||
|
|
5075b67d36 | ||
|
|
c3252f72f6 | ||
|
|
b4abb6cae8 | ||
|
|
1a3d1a024e | ||
|
|
37154ebdcd | ||
|
|
380656874e | ||
|
|
bc4afe7dd4 | ||
|
|
7c2eb31c83 | ||
|
|
737364b8d3 | ||
|
|
68874c68c5 | ||
|
|
dfadcb764b | ||
|
|
22d6bcacbb | ||
|
|
b21d86a8a3 | ||
|
|
33d0b3f0c1 | ||
|
|
f1232671ab | ||
|
|
0c402bfb6b | ||
|
|
d6d5f37077 | ||
|
|
7261a14368 | ||
|
|
0e6b5cbe5f | ||
|
|
a3dc42477a | ||
|
|
21ebc6e820 | ||
|
|
2d5f5a9230 | ||
|
|
4475e93e13 | ||
|
|
5110a762e9 | ||
|
|
a9ec12843a | ||
|
|
f0dcf9637c | ||
|
|
196ace3c04 | ||
|
|
82e264552e | ||
|
|
c134a769ea | ||
|
|
390cb9c36a | ||
|
|
9300b59f65 | ||
|
|
e1d7be8e57 | ||
|
|
90c1c327ac | ||
|
|
83032e273b | ||
|
|
a8f1569b04 | ||
|
|
0ea08e901e | ||
|
|
887b8da684 | ||
|
|
7d21e3b2fa | ||
|
|
4d270a96a8 | ||
|
|
bcc7a82a3a | ||
|
|
8b465a12ba | ||
|
|
ee68c918a1 | ||
|
|
d7075b66bd | ||
|
|
bbcea1f5e8 | ||
|
|
5ba1c3faa4 | ||
|
|
e27427f795 | ||
|
|
a32a4ce3be | ||
|
|
bc6b125b24 | ||
|
|
9c938be452 | ||
|
|
5c8182860c | ||
|
|
1d31e4647c | ||
|
|
517e7b60e1 | ||
|
|
72b4226ee9 | ||
|
|
d6d941c3a7 | ||
|
|
a0113a8c64 | ||
|
|
91508c0b8d | ||
|
|
fd63f229d5 | ||
|
|
af95fb6014 | ||
|
|
65bb24b9ae | ||
|
|
332ec355a1 | ||
|
|
2c46b1873e | ||
|
|
96439c2c42 | ||
|
|
ab97f36b64 | ||
|
|
6a4451bcd9 | ||
|
|
56277e4722 | ||
|
|
7a6fd5b75d | ||
|
|
7f3c989111 | ||
|
|
bda8304107 | ||
|
|
fb9d9c7fd1 | ||
|
|
8263b412c6 | ||
|
|
3025d90951 | ||
|
|
96391467e9 | ||
|
|
c0a654b863 | ||
|
|
b821796add | ||
|
|
33d079392a | ||
|
|
7bc77a038a | ||
|
|
371ee34383 | ||
|
|
22cd1e2846 | ||
|
|
1c5df96fe8 | ||
|
|
4f96ed41ca | ||
|
|
24eb2b1cd3 | ||
|
|
0f248b111f | ||
|
|
4ba871c5d2 | ||
|
|
f94af90359 | ||
|
|
cfc5bb8196 | ||
|
|
916aeaa486 | ||
|
|
12e3590613 | ||
|
|
232265c70e | ||
|
|
02440b875c | ||
|
|
f2dbe73097 | ||
|
|
152d971bcd | ||
|
|
acd7574284 | ||
|
|
e296e4bc38 | ||
|
|
11b4a03881 | ||
|
|
0277596341 | ||
|
|
cbd9629729 | ||
|
|
0079d08ba9 | ||
|
|
effd106adf | ||
|
|
4053ad1b58 | ||
|
|
f010ca5e91 | ||
|
|
6b2b9d6cbf | ||
|
|
b6de1c89c2 | ||
|
|
a3ae2cebbf | ||
|
|
a3f11b4f3b | ||
|
|
4b7370e9e0 | ||
|
|
5bbcaaec89 | ||
|
|
3eb3162404 | ||
|
|
0cbfd8923c | ||
|
|
0a552a868c | ||
|
|
560067cd7e | ||
|
|
6a4ea10274 | ||
|
|
349bbfc5c7 | ||
|
|
32a84650c0 | ||
|
|
b19f16aba1 | ||
|
|
594822aa55 | ||
|
|
e4aa35e083 | ||
|
|
9c16eefa31 | ||
|
|
3242e1c946 | ||
|
|
4a880ca697 | ||
|
|
d1ce9af628 | ||
|
|
ec6065fa4a | ||
|
|
18b5299952 | ||
|
|
eb3f0248c2 | ||
|
|
c03cc586e3 | ||
|
|
488bcea970 | ||
|
|
d48e7a39a7 | ||
|
|
221254738a | ||
|
|
9fb0586909 | ||
|
|
8ee14a8648 | ||
|
|
0f0d6a4e02 | ||
|
|
f9bdb9b230 | ||
|
|
f34acf161d | ||
|
|
87b3cdce48 | ||
|
|
aa1131a047 | ||
|
|
bd8a7cf39d | ||
|
|
f1891540bf | ||
|
|
d15da3daae | ||
|
|
414619c36e | ||
|
|
25757fece4 | ||
|
|
78d15e892d | ||
|
|
6a39f49991 | ||
|
|
a35600cbd7 | ||
|
|
1c53640ac3 | ||
|
|
e7f2d8cba4 | ||
|
|
7322f05792 | ||
|
|
06c75fd98b | ||
|
|
c8a30a61d2 | ||
|
|
650c91c114 | ||
|
|
5a5291d66c | ||
|
|
dbcfaa18d7 | ||
|
|
923b7c7bc6 | ||
|
|
626165eeb4 | ||
|
|
638804e9c4 | ||
|
|
1686296463 | ||
|
|
85baacd18b | ||
|
|
c31c75fffc | ||
|
|
9687a9b4fb | ||
|
|
4acae1ec02 | ||
|
|
d38d90b8d9 | ||
|
|
b52da29d5d | ||
|
|
574a4d4d36 | ||
|
|
1777406a16 | ||
|
|
d3586a2afa | ||
|
|
b7cdb3b39e | ||
|
|
96ecfca428 | ||
|
|
0e616dbbcb | ||
|
|
bc2f0e48ac | ||
|
|
efcc840ff0 | ||
|
|
7897ed257d | ||
|
|
1a018878ab | ||
|
|
e15f665efc | ||
|
|
479cee3d36 | ||
|
|
09c19932d2 | ||
|
|
4221124c4e | ||
|
|
b985eaf0a2 | ||
|
|
c84b162374 | ||
|
|
4f75bf5bdb | ||
|
|
b5c549d189 | ||
|
|
208f4bcadc | ||
|
|
f0662e41f4 | ||
|
|
dfacb656d2 | ||
|
|
db03f00693 | ||
|
|
a60a383360 | ||
|
|
aa8661acfd | ||
|
|
e045fe32c9 | ||
|
|
197da6fe78 | ||
|
|
94b77b36e3 | ||
|
|
527bf0031e | ||
|
|
35aa7e1218 | ||
|
|
23b9da75b9 | ||
|
|
8a8c1f9f3b | ||
|
|
50ebddaa2d | ||
|
|
568efb4568 | ||
|
|
6ce10ab773 | ||
|
|
039f44b7b1 | ||
|
|
00dfc32a2d | ||
|
|
76cbeef208 | ||
|
|
267368fd4f | ||
|
|
4e00b27913 | ||
|
|
766bccc341 | ||
|
|
31becbd1d1 | ||
|
|
da2a6ae5e3 | ||
|
|
87fdc3b9cd | ||
|
|
30e76c7d3a | ||
|
|
85b1912529 | ||
|
|
82da4f17f5 | ||
|
|
977c4b496c | ||
|
|
48b12b4404 | ||
|
|
9356fa15d8 | ||
|
|
2dccfab532 | ||
|
|
5d3687e8c3 | ||
|
|
0cf2d9933f | ||
|
|
1734273bbe | ||
|
|
690146e311 | ||
|
|
f888502fd2 | ||
|
|
577c7ffc45 | ||
|
|
82406c61e0 | ||
|
|
07953fb9c3 | ||
|
|
8d994f60fe | ||
|
|
8a1d490820 | ||
|
|
89c6e436ea | ||
|
|
b0babb6df1 | ||
|
|
b8f86669ab | ||
|
|
7ed877a0ae | ||
|
|
ea36294cbc | ||
|
|
0d39ff6b09 | ||
|
|
ad3fd3890f | ||
|
|
3752fed282 | ||
|
|
b072e2b296 | ||
|
|
0802529031 | ||
|
|
66e6325f43 | ||
|
|
0aa48fb9e4 | ||
|
|
628f4a97e4 | ||
|
|
fca5154e7a | ||
|
|
fbd57a1afd | ||
|
|
dd4f13ce4b | ||
|
|
7b14531f24 | ||
|
|
7a86e0f8a1 | ||
|
|
c1cd0a2ddb | ||
|
|
073dbc3953 | ||
|
|
47df696dfb | ||
|
|
9ea76503dc | ||
|
|
bb5faa21f4 | ||
|
|
b32153b437 | ||
|
|
c71755fd5f | ||
|
|
7b81825144 | ||
|
|
ebd3dcb92f | ||
|
|
80dccb6a2e | ||
|
|
cf0b3588a3 | ||
|
|
b8c0d7e402 | ||
|
|
e4982dff73 | ||
|
|
8cf662e34b | ||
|
|
b154826881 | ||
|
|
dade232592 | ||
|
|
aa6b4d4edf | ||
|
|
794af778e4 | ||
|
|
4e22ae29d3 | ||
|
|
4ae14bf2f2 | ||
|
|
f805061d11 | ||
|
|
30beb6fee7 | ||
|
|
22c7a877e3 | ||
|
|
2138bbc212 | ||
|
|
5c2b3b8b65 | ||
|
|
dd13728334 | ||
|
|
f235ec0113 | ||
|
|
f182b0bb93 | ||
|
|
a851b6597f | ||
|
|
be31675fbc | ||
|
|
dea8bbf6cd | ||
|
|
be3a79c864 | ||
|
|
ba42a1f553 | ||
|
|
1cd8f55eed | ||
|
|
8603e42a6b | ||
|
|
c32bd722d3 | ||
|
|
bcdd510254 | ||
|
|
71cb1e26d7 | ||
|
|
b459f1d405 | ||
|
|
8cc0067165 | ||
|
|
f2c94aaca0 | ||
|
|
cefefa930b | ||
|
|
4a01299e31 | ||
|
|
5a23146566 | ||
|
|
4062e49914 | ||
|
|
a483ba7c27 | ||
|
|
f981a5ee8b | ||
|
|
2b960dfd9f | ||
|
|
eec08a18b6 | ||
|
|
35018eed02 | ||
|
|
de7f34bff4 | ||
|
|
72df2e4a04 | ||
|
|
0d1446857c | ||
|
|
658e5f5d1d | ||
|
|
7930dde85c | ||
|
|
b05f82d35b | ||
|
|
51bcafe323 | ||
|
|
013f8f6523 | ||
|
|
5591a4f0b4 | ||
|
|
2fa33d5c44 | ||
|
|
cedb24d494 | ||
|
|
30f5a1553d | ||
|
|
08de8a172b | ||
|
|
c108024257 | ||
|
|
dc90cd3919 | ||
|
|
0ec71a844c | ||
|
|
0c4700e990 | ||
|
|
b83921226c | ||
|
|
3cc4ad3c38 | ||
|
|
d91bfa5cb9 | ||
|
|
f67a4a6bfe | ||
|
|
d8059bad3c | ||
|
|
be2e6b85fe | ||
|
|
b9ee806724 | ||
|
|
339a075e33 | ||
|
|
93124ad2eb | ||
|
|
3a74dc8f34 | ||
|
|
3626b7a92b | ||
|
|
eea12c196e | ||
|
|
4ce5c74ab4 | ||
|
|
2d029a9f53 | ||
|
|
f268b3dbdf | ||
|
|
755533c518 | ||
|
|
dc78dfecab | ||
|
|
f98598817c | ||
|
|
060de5bbdd | ||
|
|
c698a6d6f3 | ||
|
|
20c1906912 | ||
|
|
3fb8d7ca6b | ||
|
|
55ffa23c9e | ||
|
|
cab7f76d01 | ||
|
|
e103e87f15 | ||
|
|
bca02967a9 | ||
|
|
887db0813f | ||
|
|
4f26dcf309 | ||
|
|
4bffc326ea | ||
|
|
b859dc43fc | ||
|
|
9332132239 | ||
|
|
22e9bf74a4 | ||
|
|
b495fe0fdc | ||
|
|
7d618f12d8 | ||
|
|
385a41dea2 | ||
|
|
955832e56b | ||
|
|
c116dfcdee | ||
|
|
d6520a21ce | ||
|
|
ab81adb71b | ||
|
|
6e417c9435 | ||
|
|
ddc469367a | ||
|
|
d7b998fe71 | ||
|
|
d9a07e66af | ||
|
|
0adeb4e7c5 | ||
|
|
70d8557cc3 | ||
|
|
95fc3cd424 | ||
|
|
4633705da7 | ||
|
|
3f680588cd | ||
|
|
7d8252679d | ||
|
|
ee95c1b1ed | ||
|
|
741e44b45c | ||
|
|
8e9339e880 | ||
|
|
053b57c1df | ||
|
|
b6a56f3616 | ||
|
|
11945e561c | ||
|
|
b6ad316460 | ||
|
|
5eba0785c4 | ||
|
|
6038f09d85 | ||
|
|
e3b0260871 | ||
|
|
a6ada76a9f | ||
|
|
bfc71e845b | ||
|
|
0a11b8741a | ||
|
|
6b70fbcc84 | ||
|
|
e9c9c4d6f6 | ||
|
|
abdd558075 | ||
|
|
3dbcbe7685 | ||
|
|
a2d36b8c34 | ||
|
|
5a3647bdfe | ||
|
|
477e9c0496 | ||
|
|
82d634f4a9 | ||
|
|
8e9134c4a2 | ||
|
|
dc1997b77d | ||
|
|
1a86974aa3 | ||
|
|
fb294af8e3 | ||
|
|
46c7b16111 | ||
|
|
9dac2863af | ||
|
|
9a9d1205b0 | ||
|
|
27096cdc05 | ||
|
|
6cbfc4878d | ||
|
|
f5a59b93bf | ||
|
|
5432752e51 | ||
|
|
bf912b8e08 | ||
|
|
9a30bed98c | ||
|
|
9d02c18ac2 | ||
|
|
de8600b4d9 | ||
|
|
0ea8894579 | ||
|
|
fbe7c9ead7 | ||
|
|
b8eb28877f | ||
|
|
90c6546faf | ||
|
|
4ed2ab76e2 | ||
|
|
bc43c738ba | ||
|
|
0b79af7114 | ||
|
|
a70e690fe7 | ||
|
|
72748cc45c | ||
|
|
cf771bf69a | ||
|
|
07ccdc499c | ||
|
|
d2c8f0eb5c | ||
|
|
b643dcc1c4 | ||
|
|
cd7d08b63f | ||
|
|
4c3f047735 | ||
|
|
61e69db9e4 | ||
|
|
75d56e8364 | ||
|
|
a8aca70151 | ||
|
|
cad5504455 | ||
|
|
f8e50e8e5b | ||
|
|
cfe1af2848 | ||
|
|
89c6f08e82 | ||
|
|
cde0ff7798 | ||
|
|
b0d7527250 | ||
|
|
a28b5013c5 | ||
|
|
b9a99a5c69 | ||
|
|
98f62e65a2 | ||
|
|
c130af6b06 | ||
|
|
ef21ed9ac1 | ||
|
|
cd1d026f97 | ||
|
|
848d0e060e | ||
|
|
ae75a353d0 | ||
|
|
70f96e3222 | ||
|
|
3acf679c87 | ||
|
|
22d5852208 | ||
|
|
701778a195 | ||
|
|
a0f2c84d51 | ||
|
|
f86e2387c9 | ||
|
|
26fc9558fe | ||
|
|
90dc7b19fc | ||
|
|
c681d835d5 | ||
|
|
208d8968ce | ||
|
|
aaa7d9bb13 | ||
|
|
d150a40b09 | ||
|
|
881729448c | ||
|
|
cd841d8e33 | ||
|
|
3d8a39aa4a | ||
|
|
50e92d097b | ||
|
|
bc4dfcd798 | ||
|
|
832ad10e3a | ||
|
|
0198354961 | ||
|
|
c0a5a10cff | ||
|
|
d7f48cb324 | ||
|
|
a16796a555 | ||
|
|
231e9a5ee1 | ||
|
|
b0ddee8992 | ||
|
|
5319325886 | ||
|
|
ccce63f8c8 | ||
|
|
da958395ff | ||
|
|
1737df3e33 | ||
|
|
2df426ed1d | ||
|
|
361fdb3585 | ||
|
|
838b2b8b3b | ||
|
|
f87e0485ca | ||
|
|
b794ca4a27 | ||
|
|
8f282c6305 | ||
|
|
ed118da266 | ||
|
|
c8700656be | ||
|
|
cdd1ebf81f | ||
|
|
253b1872f1 | ||
|
|
3ac76bec77 | ||
|
|
faeec8e965 | ||
|
|
e349dfe572 | ||
|
|
fdaf48fc4a | ||
|
|
7d38ee987e | ||
|
|
f1167143cd | ||
|
|
2601811cef | ||
|
|
ca7f025fd8 | ||
|
|
17bcec8abe | ||
|
|
f5eacf3283 | ||
|
|
6bbf3649f9 | ||
|
|
22b26a2e36 | ||
|
|
2a120bdcc0 | ||
|
|
0ae06c5ca8 | ||
|
|
afad8067d1 | ||
|
|
b37741da6a | ||
|
|
2b8a981050 | ||
|
|
1f65fd2bb7 | ||
|
|
9f51bbf1de | ||
|
|
475597f97c | ||
|
|
e61225bcc4 | ||
|
|
01c74f4424 | ||
|
|
882b0aded1 | ||
|
|
89e30ef20d | ||
|
|
a692c8937f | ||
|
|
8ba5e16384 | ||
|
|
b50a1ac0ef | ||
|
|
0e2f0b29cd | ||
|
|
983634a41f | ||
|
|
2c3e000f73 | ||
|
|
a6e84f7d2c | ||
|
|
d6bed20a0e | ||
|
|
18bd639f6e | ||
|
|
53f808674b | ||
|
|
9f0d902f6b | ||
|
|
df27b4d94c | ||
|
|
5188c0fae2 | ||
|
|
ba6f370147 | ||
|
|
f884bae75b | ||
|
|
fc30b20bea | ||
|
|
7a9b920c3e | ||
|
|
12a48d1e26 | ||
|
|
020ea05561 | ||
|
|
a09e48e396 | ||
|
|
b37c208d61 | ||
|
|
72feaf99fc | ||
|
|
d86aaccb0b | ||
|
|
93a3a043d3 | ||
|
|
2bc46b061c | ||
|
|
b63262cd4d | ||
|
|
f2814ed538 | ||
|
|
ff39ad93b7 | ||
|
|
9cb6cad284 | ||
|
|
cccb308c7b | ||
|
|
dc4d794776 | ||
|
|
46935212b6 | ||
|
|
74212d40d8 | ||
|
|
c3b1fced38 | ||
|
|
ef2592b5a8 | ||
|
|
7f81f05236 | ||
|
|
e8a50f6d76 | ||
|
|
5e94354875 | ||
|
|
0fcef3f941 | ||
|
|
86f22f0ec9 | ||
|
|
3f6f07a1b8 | ||
|
|
60e86d5f6e | ||
|
|
b28435860c | ||
|
|
229d7f8e22 | ||
|
|
c561786844 | ||
|
|
6bf917941f | ||
|
|
8e8312bb82 | ||
|
|
f550a3f471 | ||
|
|
4d545ab3ca | ||
|
|
fabc2f283e | ||
|
|
6b3c82aa90 | ||
|
|
3781429147 | ||
|
|
15d6515eb1 | ||
|
|
b63353bd61 | ||
|
|
e8c0c57909 | ||
|
|
58392a5221 | ||
|
|
8061fb2da8 | ||
|
|
e835751d4f | ||
|
|
0a170d0716 | ||
|
|
03828587b5 | ||
|
|
5c4b830843 | ||
|
|
cbc095ec5f | ||
|
|
2a75c64b54 | ||
|
|
1b6b2cfaa1 | ||
|
|
17c6952aec | ||
|
|
7af01ff2ce | ||
|
|
100a4455aa | ||
|
|
4191a7a53d | ||
|
|
f37e97d410 | ||
|
|
058c0db72f | ||
|
|
f8994b214e | ||
|
|
c33bbf4574 | ||
|
|
129414faa4 | ||
|
|
12ffc19ce0 | ||
|
|
9f472591cc | ||
|
|
d25dd9e31d | ||
|
|
c43d5df158 | ||
|
|
1065f632f2 | ||
|
|
ac22950f39 | ||
|
|
a8105eccb2 | ||
|
|
888277ec40 | ||
|
|
0014f9ae69 | ||
|
|
8036c78e08 | ||
|
|
516c7d9f63 | ||
|
|
54cbf59b5a | ||
|
|
ce864cebfd | ||
|
|
ed78f52cd1 | ||
|
|
ccf20eb3ff | ||
|
|
d0c1189b91 | ||
|
|
0d31fe99c7 | ||
|
|
d4b3659523 | ||
|
|
8e083c99c8 | ||
|
|
8c6d1e8e6c | ||
|
|
6dea12a067 | ||
|
|
1f311832ab | ||
|
|
e0d1307d3f | ||
|
|
f0da0bde87 | ||
|
|
a5bf48eab0 | ||
|
|
7a1a3276c4 | ||
|
|
8ca0e4dd2c | ||
|
|
1367ff5ec6 | ||
|
|
fbb45c674c | ||
|
|
eb61d44f9f | ||
|
|
c8f55b615d | ||
|
|
bd1b2a1e85 | ||
|
|
37557ffa35 | ||
|
|
6cd917c227 | ||
|
|
8278a36318 | ||
|
|
f8b559dacd | ||
|
|
add09c8034 | ||
|
|
ea50f65f89 | ||
|
|
e1bf6ef8cb | ||
|
|
20c142f749 | ||
|
|
e4c9268b19 | ||
|
|
2e88633ba4 | ||
|
|
25cc5655c0 | ||
|
|
df83e5c7fc | ||
|
|
3cb73d08c6 | ||
|
|
ef2482ddec | ||
|
|
e3b09be7f0 | ||
|
|
bcf279278d | ||
|
|
68d4460361 | ||
|
|
f3f521f038 | ||
|
|
e75f0d27ef | ||
|
|
e63432fbfc | ||
|
|
627e90c304 | ||
|
|
1bfe2485bb | ||
|
|
4a2d51a73a | ||
|
|
6a768cb679 | ||
|
|
e55bd4ba7a | ||
|
|
61bffa4feb | ||
|
|
2d087ee4b7 | ||
|
|
069c05e44f | ||
|
|
b3563e3d6a | ||
|
|
d609bef9fd | ||
|
|
4731f0cf31 | ||
|
|
504bb8ec8c | ||
|
|
1d3f51e100 | ||
|
|
04fd3a7db5 | ||
|
|
eda5713eab | ||
|
|
3e9fd0665c | ||
|
|
a49a19ffa3 | ||
|
|
9102faa4b3 | ||
|
|
03e7590429 | ||
|
|
e2a4da027c | ||
|
|
e968b3d252 | ||
|
|
858b3516a0 | ||
|
|
4475fbb502 | ||
|
|
6d98f2462b | ||
|
|
6f3a2b38f0 | ||
|
|
0ab2b33064 | ||
|
|
983e7800b6 | ||
|
|
be683e2ac3 |
@@ -6,4 +6,6 @@ ignore = [
|
||||
"RUSTSEC-2020-0095",
|
||||
# proc-macro-error is unmaintained
|
||||
"RUSTSEC-2024-0370",
|
||||
# time crate can't be updated in the repo because of MSRV, users are unaffected
|
||||
"RUSTSEC-2026-0009",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'tauri': 'minor:feat'
|
||||
'@tauri-apps/api': 'minor:feat'
|
||||
---
|
||||
|
||||
Add Bring All to Front predefined menu item type
|
||||
5
.changes/base64.md
Normal file
5
.changes/base64.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri-macos-sign": patch:enhance
|
||||
---
|
||||
|
||||
Do not rely on system base64 CLI to decode certificates.
|
||||
6
.changes/change-pr-15117.md
Normal file
6
.changes/change-pr-15117.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"tauri-build": patch:enhance
|
||||
"tauri": patch:enhance
|
||||
---
|
||||
|
||||
Simplify async-sync code boundaries, no externally visible changes
|
||||
@@ -4,7 +4,7 @@
|
||||
"feat": "New Features",
|
||||
"enhance": "Enhancements",
|
||||
"bug": "Bug Fixes",
|
||||
"pref": "Performance Improvements",
|
||||
"perf": "Performance Improvements",
|
||||
"changes": "What's Changed",
|
||||
"sec": "Security fixes",
|
||||
"deps": "Dependencies",
|
||||
@@ -27,12 +27,6 @@
|
||||
"dryRunCommand": true,
|
||||
"pipe": true
|
||||
},
|
||||
{
|
||||
"command": "cargo generate-lockfile",
|
||||
"dryRunCommand": true,
|
||||
"runFromRoot": true,
|
||||
"pipe": true
|
||||
},
|
||||
{
|
||||
"command": "cargo audit ${ process.env.CARGO_AUDIT_OPTIONS || '' }",
|
||||
"dryRunCommand": true,
|
||||
@@ -107,7 +101,7 @@
|
||||
"pipe": true
|
||||
},
|
||||
{
|
||||
"command": "pnpm publish --access public --loglevel silly --tag next --no-git-checks",
|
||||
"command": "pnpm publish --access public --loglevel silly --no-git-checks",
|
||||
"dryRunCommand": "npm publish --dry-run --access public --no-git-checks",
|
||||
"pipe": true
|
||||
},
|
||||
|
||||
5
.changes/data-tauri-drag-region-deep.md
Normal file
5
.changes/data-tauri-drag-region-deep.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri": minor:feat
|
||||
---
|
||||
|
||||
Add `data-tauri-drag-region="deep"` so clicks on non-clickable children will drag as well. Can still opt out of drag on some regions using `data-tauri-drag-region="false"`
|
||||
7
.changes/eval-with-callback.md
Normal file
7
.changes/eval-with-callback.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'tauri': 'minor:feat'
|
||||
'tauri-runtime': 'minor:feat'
|
||||
'tauri-runtime-wry': 'minor:feat'
|
||||
---
|
||||
|
||||
Add `eval_with_callback` to the Tauri webview APIs and runtime dispatch layers.
|
||||
13
.changes/feat-uninstaller-icon-image.md
Normal file
13
.changes/feat-uninstaller-icon-image.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
"tauri-bundler": minor:feat
|
||||
"tauri-cli": minor:feat
|
||||
"@tauri-apps/cli": minor:feat
|
||||
"tauri-utils": minor:feat
|
||||
---
|
||||
|
||||
Added uninstaller icon and uninstaller header image support for NSIS installer.
|
||||
|
||||
Notes:
|
||||
|
||||
- For `tauri-bundler` lib users, the `NsisSettings` now has 2 new fields `uninstaller_icon` and `uninstaller_header_image` which can be a breaking change
|
||||
- When bundling with NSIS, users can add `uninstallerIcon` and `uninstallerHeaderImage` under `bundle > windows > nsis` to configure them.
|
||||
7
.changes/fix-build-bundles-arg.md
Normal file
7
.changes/fix-build-bundles-arg.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"tauri-bundler": patch:bug
|
||||
"tauri-cli": patch:bug
|
||||
"@tauri-apps/cli": patch:bug
|
||||
---
|
||||
|
||||
Fix `build --bundles` to allow `nsis` arg in linux+macOS
|
||||
6
.changes/fix-ios-metal-toolchain.md
Normal file
6
.changes/fix-ios-metal-toolchain.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"tauri-cli": patch:bug
|
||||
"@tauri-apps/cli": patch:bug
|
||||
---
|
||||
|
||||
Fix iOS build failure when `Metal Toolchain` is installed by using explicit `$(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain` path instead of `$(TOOLCHAIN_DIR)` for Swift library search paths.
|
||||
5
.changes/linux-deploy-link.md
Normal file
5
.changes/linux-deploy-link.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri-bundler": patch:bug
|
||||
---
|
||||
|
||||
Correct GitHub Release URL path for Linux i686 tooling.
|
||||
5
.changes/liquid-glass-icon.md
Normal file
5
.changes/liquid-glass-icon.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri-bundler": minor:feat
|
||||
---
|
||||
|
||||
Added support to Liquid Glass icons.
|
||||
8
.changes/multi-window-mobile.md
Normal file
8
.changes/multi-window-mobile.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"tauri": minor:feat
|
||||
"tauri-runtime-wry": minor:feat
|
||||
"tauri-runtime": minor:feat
|
||||
"tauri-utils": minor:feat
|
||||
---
|
||||
|
||||
Support creating multiple windows on Android (activity embedding) and iOS (scenes).
|
||||
6
.changes/new-window-main-thread.md
Normal file
6
.changes/new-window-main-thread.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"tauri": minor:changes
|
||||
"tauri-runtime-wry": minor:changes
|
||||
---
|
||||
|
||||
The new window handler passed to `on_new_window` no longer requires `Sync`, and runs on main thread on Windows, aligning with other platforms
|
||||
6
.changes/prompt-signing-key-password-context.md
Normal file
6
.changes/prompt-signing-key-password-context.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"tauri-cli": patch:enhance
|
||||
"@tauri-apps/cli": patch:enhance
|
||||
---
|
||||
|
||||
Show the context before prompting for updater signing key password
|
||||
5
.changes/safepathbuf_into_pathbuf.md
Normal file
5
.changes/safepathbuf_into_pathbuf.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri": patch:enhance
|
||||
---
|
||||
|
||||
Implement retrieving inner PathBuf from SafePathBuf to ease using APIs that require an owned PathBuf
|
||||
6
.changes/supersede-kuchikiki.md
Normal file
6
.changes/supersede-kuchikiki.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"tauri-utils": minor:deps
|
||||
---
|
||||
|
||||
Add new `html-manipulation-2` and `build-2` feature flags that use `dom_query` instead of `kuchikiki` for HTML parsing / manipulation.
|
||||
This allows downstream users to remove `kuchikiki` and its dependencies from their dependency tree.
|
||||
5
.changes/tauri-dbus.md
Normal file
5
.changes/tauri-dbus.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
tauri: minor:feat
|
||||
---
|
||||
|
||||
Added `dbus` feature flag (enabled by default) which is required for theme detection on Linux.
|
||||
5
.changes/toml-ver.md
Normal file
5
.changes/toml-ver.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
tauri-utils: patch:deps
|
||||
---
|
||||
|
||||
Changed `toml` crate version from `0.9` to `">=0.9, <=1"`
|
||||
12
.changes/wix-minimum-webview2-version.md
Normal file
12
.changes/wix-minimum-webview2-version.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
"tauri-bundler": minor:feat
|
||||
"tauri-cli": minor:feat
|
||||
"@tauri-apps/cli": minor:feat
|
||||
---
|
||||
|
||||
Added support for `minimumWebview2Version` option support for the MSI (Wix) installer, the old `bundle > windows > nsis > minimumWebview2Version` is now deprecated in favor of `bundle > windows > minimumWebview2Version`
|
||||
|
||||
Notes:
|
||||
|
||||
- For anyone relying on the `WVRTINSTALLED` `Property` tag in `main.wxs`, it is now renamed to `INSTALLED_WEBVIEW2_VERSION`
|
||||
- For `tauri-bundler` lib users, the `WindowsSettings` now has a new field `minimum_webview2_version` which can be a breaking change
|
||||
56
.github/CONTRIBUTING.md
vendored
56
.github/CONTRIBUTING.md
vendored
@@ -5,6 +5,7 @@ Hi! We, the maintainers, are really excited that you are interested in contribut
|
||||
- [Issue Reporting Guidelines](#issue-reporting-guidelines)
|
||||
- [Pull Request Guidelines](#pull-request-guidelines)
|
||||
- [Development Guide](#development-guide)
|
||||
- [AI Tool Policy](#ai-tool-policy)
|
||||
|
||||
## Issue Reporting Guidelines
|
||||
|
||||
@@ -33,11 +34,9 @@ Hi! We, the maintainers, are really excited that you are interested in contribut
|
||||
- It's OK to have multiple small commits as you work on the PR - we will let GitHub automatically squash it before merging.
|
||||
|
||||
- If adding new feature:
|
||||
|
||||
- Provide convincing reason to add this feature. Ideally you should open a suggestion issue first and have it greenlighted before working on it.
|
||||
|
||||
- If fixing a bug:
|
||||
|
||||
- If you are resolving a special issue, add `(fix: #xxxx[,#xxx])` (#xxxx is the issue id) in your PR title for a better release log, e.g. `fix: update entities encoding/decoding (fix #3899)`.
|
||||
- Provide detailed description of the bug in the PR, or link to an issue that does.
|
||||
|
||||
@@ -51,37 +50,70 @@ Hi! We, the maintainers, are really excited that you are interested in contribut
|
||||
|
||||
First, [join our Discord server](https://discord.gg/SpmNs4S) and let us know that you want to contribute. This way we can point you in the right direction and help ensure your contribution will be as helpful as possible.
|
||||
|
||||
To set up your machine for development, follow the [Tauri setup guide](https://v2.tauri.app/start/prerequisites/) to get all the tools you need to develop Tauri apps. The only additional tool you may need is [PNPM](https://pnpm.io/), it is only required if you are developing the Node CLI or API packages (`packages/cli` and `packages/api`). Next, fork and clone this repo. It is structured as a monorepo, which means that all the various Tauri packages are under the same repository. The development process varies depending on what part of Tauri you are contributing to, see the guides below for per-package instructions.
|
||||
To set up your machine for development, follow the [Tauri setup guide](https://v2.tauri.app/start/prerequisites/) to get all the tools you need to develop Tauri apps. The only additional tool you may need is [PNPM](https://pnpm.io/), it is only required if you are developing the Node CLI or API packages (`packages/cli` and `packages/api`).
|
||||
|
||||
Some Tauri packages will be automatically built when running one of the examples. Others, however, will need to be built beforehand. To build these automatically, run the `.scripts/setup.sh` (Linux and macOS) or `.scripts/setup.ps1` (Windows) script. This will install the Rust and Node.js CLI and build the JS API. After that, you should be able to run all the examples. Note that the setup script should be executed from the root folder of the repository in order to run correctly.
|
||||
Next, [fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) and clone [this repository](https://github.com/tauri-apps).
|
||||
The development process varies depending on what part of Tauri you are contributing to, see the guides below for per-package instructions.
|
||||
|
||||
Some Tauri packages will be automatically built when running one of the examples. Others, however, will need to be built beforehand. To initialize, execute these commands in the repository root:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm build
|
||||
```
|
||||
|
||||
### Overview
|
||||
|
||||
See [Architecture](../ARCHITECTURE.md#major-components) for an overview of the packages in this repository.
|
||||
|
||||
### Developing Tauri Core and Related Components (Rust API, Macros, Codegen, and Utils)
|
||||
|
||||
The code for the Rust crates, including the Core, Macros, Utils, WRY runtime, and a few more are located in the [main Tauri repository](https://github.com/tauri-apps/tauri/tree/dev/crates).
|
||||
|
||||
The easiest way to test your changes is to use the [helloworld](https://github.com/tauri-apps/tauri/tree/dev/examples/helloworld) example app. It automatically rebuilds and uses your local copy of the Tauri core packages. Just run `cargo run --example helloworld` after making changes to test them out.
|
||||
|
||||
To test local changes against your own application simply point the Tauri create to your local repository. In `src-tauri/Cargo.toml` file change:
|
||||
|
||||
`tauri = { version = "2.1.1" }`
|
||||
|
||||
to:
|
||||
|
||||
`tauri = { path = "path/to/local/tauri/crates/tauri" }`
|
||||
|
||||
If any other crates depend on Tauri you will have to point them to the local repo as well.
|
||||
|
||||
### Developing Tauri Bundler and Rust CLI
|
||||
|
||||
The code for the bundler is located in `[Tauri repo root]/crates/tauri-bundler`, and the code for the Rust CLI is located in `[Tauri repo root]/crates/tauri-cli`. If you are using your local copy of `@tauri-apps/cli` (see above), any changes you make to the bundler and CLI will be automatically built and applied when running the build or dev command. Otherwise, running `cargo install --path .` in the Rust CLI directory will allow you to run `cargo tauri build` and `cargo tauri dev` anywhere, using the updated copy of the bundler and cli. You will have to run this command each time you make a change in either package.
|
||||
The code for the bundler is located in [crates/tauri-bundler](https://github.com/tauri-apps/tauri/tree/dev/crates/tauri-bundler), and the code for the Rust CLI is located in [tauri-cli](https://github.com/tauri-apps/tauri/tree/dev/crates/tauri-cli).
|
||||
Running `cargo install --path .` in the Rust CLI directory will allow you to run `cargo tauri build` and `cargo tauri dev` anywhere, using the updated copy of the bundler and cli. You will have to run this command each time you make a change in either package.
|
||||
You can use `cargo install --path . --debug` to speed up test builds.
|
||||
|
||||
### Developing The Node.js CLI (`@tauri-apps/cli`)
|
||||
|
||||
`@tauri-apps/cli` is a wrapper to `tauri-cli` so most changes should be written on the Rust CLI. The `[Tauri repo root]/crates/tauri-cli` folder contains only packaging scripts to properly publish the Rust CLI binaries to NPM.
|
||||
|
||||
### Developing Tauri Core and Related Components (Rust API, Macros, Codegen, and Utils)
|
||||
|
||||
The code for the Rust crates, including the Core, Macros, Utils, WRY runtime, and a few more are located in `[Tauri repo root]/crates/tauri-(macros/utils)`. The easiest way to test your changes is to use the `[Tauri repo root]/examples/helloworld` app. It automatically rebuilds and uses your local copy of the Tauri core packages. Just run `cargo run --example helloworld` after making changes to test them out.
|
||||
[`@tauri-apps/cli`](https://github.com/tauri-apps/tauri/tree/dev/packages/cli) is a small wrapper around `tauri-cli` so most changes should be happen in the Rust CLI (see above).
|
||||
|
||||
#### Building the documentation locally
|
||||
|
||||
You can build the Rust documentation locally running the following script:
|
||||
|
||||
```bash
|
||||
$ RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features --open
|
||||
$ cargo +nightly doc --all-features --open
|
||||
```
|
||||
|
||||
### Developing the JS API
|
||||
|
||||
The JS API provides bindings between the developer's JS in the Webview and the builtin Tauri APIs, written in Rust. Its code is located in `[Tauri repo root]/packages/api`. After making changes to the code, run `pnpm build` to build it. To test your changes, we recommend using the API example app, located in `[Tauri repo root]/examples/api`. It will automatically use your local copy of the JS API and provides a helpful UI to test the various commands.
|
||||
The JS API provides bindings between the developer's JS in the Webview and the built-in Tauri APIs, written in Rust. Its code is located in [/packages/api](https://github.com/tauri-apps/tauri/tree/dev/packages/api).
|
||||
After making changes to the code, run `pnpm build` to build it. To test your changes, we recommend using the API example app, located in [/examples/api](https://github.com/tauri-apps/tauri/tree/dev/examples/api). It will automatically use your local copy of the JS API and provides a helpful UI to test the various commands.
|
||||
|
||||
## AI Tool Policy
|
||||
|
||||
It takes a lot of time to review a Pull Request while it's very easy to make a nonsensical but plausible looking one using AI tools.
|
||||
It is unfair for other contributors and the reviewers to spend much of the time dealing with this, hence these rules:
|
||||
|
||||
1. Review and test all LLM-generated content before submitting, you're the one responsible for it, not the AI.
|
||||
2. Don't use AI to respond to review comments (except for translations).
|
||||
|
||||
We will close the Pull Request with a `ai-slop` tag if you failed to do so.
|
||||
|
||||
## Financial Contribution
|
||||
|
||||
|
||||
5
.github/RELEASING.md
vendored
5
.github/RELEASING.md
vendored
@@ -33,11 +33,6 @@ Releasing can be as easy as merging the version pull request but here is a check
|
||||
|
||||
- [ ] Double check that every package is bumped correctly and there are no accidental major or minor being released unless that is indeed the intention.
|
||||
- [ ] Make sure that there are no pending or unfinished [covector-version-or-publish.yml](./workflows/covector-version-or-publish.yml) workflow runs.
|
||||
- [ ] Sign the Version PR before merging as we require signed commits
|
||||
- [ ] `git fetch --all`
|
||||
- [ ] `git checkout release/version-updates`
|
||||
- [ ] `git commit --amend -S`
|
||||
- [ ] `git push --force`
|
||||
- [ ] Approve and merge the version pull request
|
||||
|
||||
## Publishing failed, what to do?
|
||||
|
||||
13
.github/workflows/audit.yml
vendored
13
.github/workflows/audit.yml
vendored
@@ -8,7 +8,16 @@ on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/audit.yml'
|
||||
- '**/Cargo.lock'
|
||||
- '**/Cargo.toml'
|
||||
- '**/package.json'
|
||||
- '**/pnpm-lock.yaml'
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- '.github/workflows/audit.yml'
|
||||
- '**/Cargo.lock'
|
||||
@@ -26,7 +35,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: rust audit
|
||||
uses: rustsec/audit-check@v1
|
||||
uses: rustsec/audit-check@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -34,7 +43,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: corepack enable
|
||||
- run: npm i -g --force corepack
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
|
||||
4
.github/workflows/check-generated-files.yml
vendored
4
.github/workflows/check-generated-files.yml
vendored
@@ -8,6 +8,7 @@ on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/check-generated-files.yml'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'packages/api/src/**'
|
||||
- 'crates/tauri/scripts/bundle.global.js'
|
||||
- 'crates/tauri-utils/src/config.rs'
|
||||
@@ -31,6 +32,7 @@ jobs:
|
||||
with:
|
||||
filters: |
|
||||
api:
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'packages/api/src/**'
|
||||
- 'crates/tauri/scripts/bundle.global.js'
|
||||
schema:
|
||||
@@ -44,7 +46,7 @@ jobs:
|
||||
if: needs.changes.outputs.api == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: corepack enable
|
||||
- run: npm i -g --force corepack
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- run: corepack enable
|
||||
- run: npm i -g --force corepack
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
if: matrix.platform == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y webkit2gtk-4.1 libayatana-appindicator3-dev libfuse2
|
||||
sudo apt-get install -y webkit2gtk-4.1 libayatana-appindicator3-dev libfuse2 librsvg2-dev
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
actions: write # required for workflow_dispatch
|
||||
contents: write # required to create new releases
|
||||
pull-requests: write # required to open version update pr
|
||||
id-token: write # pnpm provenance
|
||||
id-token: write # pnpm provenance / oidc token
|
||||
outputs:
|
||||
change: ${{ steps.covector.outputs.change }}
|
||||
commandRan: ${{ steps.covector.outputs.commandRan }}
|
||||
@@ -73,12 +73,10 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4
|
||||
- run: npm i -g --force corepack
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'pnpm'
|
||||
node-version: 24
|
||||
|
||||
- name: cargo login
|
||||
run: cargo login ${{ secrets.ORG_CRATES_IO_TOKEN }}
|
||||
@@ -90,13 +88,12 @@ jobs:
|
||||
- name: install Linux dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y webkit2gtk-4.1 libayatana-appindicator3-dev
|
||||
sudo apt-get install -y webkit2gtk-4.1 libayatana-appindicator3-dev librsvg2-dev
|
||||
|
||||
- name: covector version or publish (publish when no change files present)
|
||||
uses: jbolda/covector/packages/action@covector-v0
|
||||
id: covector
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.ORG_NPM_TOKEN }}
|
||||
CARGO_AUDIT_OPTIONS: ${{ secrets.CARGO_AUDIT_OPTIONS }}
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
with:
|
||||
@@ -111,7 +108,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request With Versions Bumped
|
||||
if: steps.covector.outputs.commandRan == 'version'
|
||||
uses: tauri-apps/create-pull-request@v3
|
||||
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # 7.0.6
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: release/version-updates
|
||||
@@ -119,12 +116,13 @@ jobs:
|
||||
commit-message: 'apply version updates'
|
||||
labels: 'version updates'
|
||||
body: ${{ steps.covector.outputs.change }}
|
||||
sign-commits: true
|
||||
|
||||
- name: Trigger doc update
|
||||
if: |
|
||||
steps.covector.outputs.successfulPublish == 'true' &&
|
||||
steps.covector.outputs.packagesPublished != ''
|
||||
uses: peter-evans/repository-dispatch@v1
|
||||
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # 3.0.0
|
||||
with:
|
||||
token: ${{ secrets.ORG_TAURI_BOT_PAT }}
|
||||
repository: tauri-apps/tauri-docs
|
||||
|
||||
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@@ -66,7 +66,7 @@ jobs:
|
||||
with:
|
||||
targets: ${{ matrix.target.name }}
|
||||
|
||||
- run: corepack enable
|
||||
- run: npm i -g --force corepack
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
|
||||
2
.github/workflows/fmt.yml
vendored
2
.github/workflows/fmt.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: corepack enable
|
||||
- run: npm i -g --force corepack
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
|
||||
4
.github/workflows/lint-js.yml
vendored
4
.github/workflows/lint-js.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: corepack enable
|
||||
- run: npm i -g --force corepack
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: corepack enable
|
||||
- run: npm i -g --force corepack
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
|
||||
2
.github/workflows/lint-rust.yml
vendored
2
.github/workflows/lint-rust.yml
vendored
@@ -10,7 +10,7 @@ on:
|
||||
- dev
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/lint-cli.yml'
|
||||
- '.github/workflows/lint-rust.yml'
|
||||
- 'crates/**'
|
||||
|
||||
env:
|
||||
|
||||
56
.github/workflows/publish-cli-js.yml
vendored
56
.github/workflows/publish-cli-js.yml
vendored
@@ -20,6 +20,10 @@ defaults:
|
||||
run:
|
||||
working-directory: packages/cli/
|
||||
|
||||
permissions:
|
||||
contents: write # update release
|
||||
id-token: write # oidc token
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
@@ -43,15 +47,16 @@ jobs:
|
||||
- host: windows-latest
|
||||
architecture: x64
|
||||
target: aarch64-pc-windows-msvc
|
||||
build: pnpm build --target aarch64-pc-windows-msvc --features native-tls-vendored --cargo-flags="--no-default-features"
|
||||
- host: ubuntu-20.04
|
||||
build: pnpm build --target aarch64-pc-windows-msvc
|
||||
- host: ubuntu-22.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
|
||||
build: |
|
||||
npm i -g --force corepack
|
||||
cd packages/cli
|
||||
pnpm build --target x86_64-unknown-linux-gnu
|
||||
strip *.node
|
||||
- host: ubuntu-20.04
|
||||
- host: ubuntu-22.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
|
||||
build: |
|
||||
@@ -63,14 +68,15 @@ jobs:
|
||||
build: |
|
||||
pnpm build --features native-tls-vendored --target=aarch64-apple-darwin
|
||||
strip -x *.node
|
||||
- host: ubuntu-20.04
|
||||
- host: ubuntu-22.04
|
||||
target: aarch64-unknown-linux-gnu
|
||||
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
|
||||
build: |
|
||||
npm i -g --force corepack
|
||||
cd packages/cli
|
||||
pnpm build --target aarch64-unknown-linux-gnu
|
||||
aarch64-unknown-linux-gnu-strip *.node
|
||||
- host: ubuntu-20.04
|
||||
- host: ubuntu-22.04
|
||||
architecture: x64
|
||||
target: armv7-unknown-linux-gnueabihf
|
||||
setup: |
|
||||
@@ -79,7 +85,7 @@ jobs:
|
||||
build: |
|
||||
pnpm build --target=armv7-unknown-linux-gnueabihf
|
||||
arm-linux-gnueabihf-strip *.node
|
||||
- host: ubuntu-20.04
|
||||
- host: ubuntu-22.04
|
||||
architecture: x64
|
||||
target: aarch64-unknown-linux-musl
|
||||
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
|
||||
@@ -88,11 +94,20 @@ jobs:
|
||||
rustup target add aarch64-unknown-linux-musl
|
||||
pnpm build --target aarch64-unknown-linux-musl
|
||||
/aarch64-linux-musl-cross/bin/aarch64-linux-musl-strip *.node
|
||||
- host: ubuntu-22.04
|
||||
architecture: x64
|
||||
target: riscv64gc-unknown-linux-gnu
|
||||
setup: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install gcc-riscv64-linux-gnu g++-riscv64-linux-gnu -y
|
||||
build: |
|
||||
pnpm build --target=riscv64gc-unknown-linux-gnu
|
||||
riscv64-linux-gnu-strip *.node
|
||||
name: stable - ${{ matrix.settings.target }} - node@20
|
||||
runs-on: ${{ matrix.settings.host }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: corepack enable
|
||||
- run: npm i -g --force corepack
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
if: ${{ !matrix.settings.docker }}
|
||||
@@ -105,10 +120,9 @@ jobs:
|
||||
if: ${{ !matrix.settings.docker }}
|
||||
with:
|
||||
targets: ${{ matrix.settings.target }}
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: ${{ matrix.settings.target }}
|
||||
working-directory: 'crates/tauri-cli/'
|
||||
if: ${{ matrix.settings.docker }}
|
||||
- name: Setup toolchain
|
||||
run: ${{ matrix.settings.setup }}
|
||||
@@ -203,7 +217,7 @@ jobs:
|
||||
runs-on: ${{ matrix.settings.host }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: corepack enable
|
||||
- run: npm i -g --force corepack
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -234,7 +248,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: corepack enable
|
||||
- run: npm i -g --force corepack
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -253,7 +267,7 @@ jobs:
|
||||
- name: install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y webkit2gtk-4.1 libayatana-appindicator3-dev
|
||||
sudo apt-get install -y webkit2gtk-4.1 libayatana-appindicator3-dev librsvg2-dev
|
||||
- name: Test bindings
|
||||
run: pnpm test
|
||||
test-linux-x64-musl-binding:
|
||||
@@ -271,7 +285,7 @@ jobs:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: corepack enable
|
||||
- run: npm i -g --force corepack
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -309,7 +323,6 @@ jobs:
|
||||
- '20'
|
||||
image:
|
||||
- ghcr.io/napi-rs/napi-rs/nodejs:aarch64-16
|
||||
- ghcr.io/napi-rs/napi-rs/nodejs:armhf-16
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: docker run --rm --privileged multiarch/qemu-user-static:register --reset
|
||||
@@ -357,16 +370,13 @@ jobs:
|
||||
- test-linux-x64-gnu-binding
|
||||
- test-linux-x64-musl-binding
|
||||
#- test-linux-arm-bindings
|
||||
permissions:
|
||||
contents: write # update release
|
||||
id-token: write # npm provenance
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: corepack enable
|
||||
- run: npm i -g --force corepack
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 24
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm i --frozen-lockfile --ignore-scripts
|
||||
@@ -381,10 +391,8 @@ jobs:
|
||||
shell: bash
|
||||
- name: Publish
|
||||
run: |
|
||||
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
|
||||
npm publish --tag next
|
||||
npm publish
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.ORG_NPM_TOKEN }}
|
||||
NODE_AUTH_TOKEN: ''
|
||||
RELEASE_ID: ${{ github.event.client_payload.releaseId || inputs.releaseId }}
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
|
||||
33
.github/workflows/publish-cli-rs.yml
vendored
33
.github/workflows/publish-cli-rs.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
config:
|
||||
- os: ubuntu-20.04
|
||||
- os: ubuntu-22.04
|
||||
rust_target: x86_64-unknown-linux-gnu
|
||||
ext: ''
|
||||
args: ''
|
||||
@@ -37,36 +37,63 @@ jobs:
|
||||
- os: windows-latest
|
||||
rust_target: aarch64-pc-windows-msvc
|
||||
ext: '.exe'
|
||||
args: '--no-default-features --features native-tls-vendored'
|
||||
args: ''
|
||||
- os: ubuntu-22.04
|
||||
rust_target: riscv64gc-unknown-linux-gnu
|
||||
ext: ''
|
||||
args: ''
|
||||
cross: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: 'Setup Rust'
|
||||
if: ${{ !matrix.config.cross }}
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.config.rust_target }}
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
if: ${{ !matrix.config.cross }}
|
||||
with:
|
||||
key: ${{ matrix.config.rust_target }}
|
||||
|
||||
- name: install Linux dependencies
|
||||
if: matrix.config.os == 'ubuntu-latest'
|
||||
if: ${{ !matrix.config.cross && startsWith(matrix.config.os, 'ubuntu') }}
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev
|
||||
|
||||
- name: Install cross
|
||||
if: ${{ matrix.config.cross }}
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cross@0.2.5
|
||||
|
||||
- name: Build CLI
|
||||
if: ${{ !matrix.config.cross }}
|
||||
run: cargo build --manifest-path ./crates/tauri-cli/Cargo.toml --profile release-size-optimized ${{ matrix.config.args }}
|
||||
|
||||
- name: Build CLI (cross)
|
||||
if: ${{ matrix.config.cross }}
|
||||
run: cross build --manifest-path ./crates/tauri-cli/Cargo.toml --target ${{ matrix.config.rust_target }} --profile release-size-optimized ${{ matrix.config.args }}
|
||||
|
||||
- name: Upload CLI
|
||||
if: ${{ !matrix.config.cross }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cargo-tauri-${{ matrix.config.rust_target }}${{ matrix.config.ext }}
|
||||
path: target/release-size-optimized/cargo-tauri${{ matrix.config.ext }}
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload CLI (cross)
|
||||
if: ${{ matrix.config.cross }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cargo-tauri-${{ matrix.config.rust_target }}${{ matrix.config.ext }}
|
||||
path: target/${{ matrix.config.rust_target }}/release-size-optimized/cargo-tauri${{ matrix.config.ext }}
|
||||
if-no-files-found: error
|
||||
|
||||
upload:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
2
.github/workflows/supply-chain.yml
vendored
2
.github/workflows/supply-chain.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
run: rustup update stable && rustup default stable
|
||||
|
||||
- uses: actions/cache@v2
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ runner.tool_cache }}/cargo-vet
|
||||
key: cargo-vet-bin-${{ env.CARGO_VET_VERSION }}
|
||||
|
||||
6
.github/workflows/test-android.yml
vendored
6
.github/workflows/test-android.yml
vendored
@@ -33,8 +33,8 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: install Rust 1.77.2
|
||||
uses: dtolnay/rust-toolchain@1.77.2
|
||||
|
||||
- name: install Linux dependencies
|
||||
if: matrix.platform == 'ubuntu-latest'
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.1
|
||||
|
||||
- run: corepack enable
|
||||
- run: npm i -g --force corepack
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
|
||||
5
.github/workflows/test-cli-js.yml
vendored
5
.github/workflows/test-cli-js.yml
vendored
@@ -11,6 +11,7 @@ on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/test-cli-js.yml'
|
||||
- 'packages/cli/**'
|
||||
# currently` @tauri-apps/cli` only tests the template
|
||||
- 'crates/tauri-cli/templates/app/**'
|
||||
|
||||
@@ -37,7 +38,7 @@ jobs:
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- run: corepack enable
|
||||
- run: npm i -g --force corepack
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -48,7 +49,7 @@ jobs:
|
||||
if: matrix.platform == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y webkit2gtk-4.1 libayatana-appindicator3-dev
|
||||
sudo apt-get install -y webkit2gtk-4.1 libayatana-appindicator3-dev librsvg2-dev
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
|
||||
4
.github/workflows/test-cli-rs.yml
vendored
4
.github/workflows/test-cli-rs.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: 'Setup Rust'
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@1.77.2
|
||||
with:
|
||||
targets: ${{ matrix.platform.target }}
|
||||
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
if: matrix.platform.os == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.1 libayatana-appindicator3-dev
|
||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.1 libayatana-appindicator3-dev librsvg2-dev
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
|
||||
34
.github/workflows/test-core.yml
vendored
34
.github/workflows/test-core.yml
vendored
@@ -37,35 +37,35 @@ jobs:
|
||||
- {
|
||||
target: x86_64-pc-windows-msvc,
|
||||
os: windows-latest,
|
||||
toolchain: '1.78.0',
|
||||
toolchain: '1.77.2',
|
||||
cross: false,
|
||||
command: 'test'
|
||||
}
|
||||
- {
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
os: ubuntu-latest,
|
||||
toolchain: '1.78.0',
|
||||
toolchain: '1.77.2',
|
||||
cross: false,
|
||||
command: 'test'
|
||||
}
|
||||
- {
|
||||
target: aarch64-apple-darwin,
|
||||
os: macos-14,
|
||||
toolchain: '1.78.0',
|
||||
toolchain: '1.77.2',
|
||||
cross: false,
|
||||
command: 'test'
|
||||
}
|
||||
- {
|
||||
target: aarch64-apple-ios,
|
||||
os: macos-latest,
|
||||
toolchain: '1.78.0',
|
||||
toolchain: '1.77.2',
|
||||
cross: false,
|
||||
command: 'build'
|
||||
}
|
||||
- {
|
||||
target: aarch64-linux-android,
|
||||
os: ubuntu-latest,
|
||||
toolchain: '1.78.0',
|
||||
toolchain: '1.77.2',
|
||||
cross: true,
|
||||
command: 'build'
|
||||
}
|
||||
@@ -90,15 +90,27 @@ jobs:
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: v2
|
||||
key: ${{ matrix.platform.target }}
|
||||
save-if: ${{ matrix.features.key == 'all' }}
|
||||
|
||||
- name: test
|
||||
- name: test tauri-utils
|
||||
if: ${{ !matrix.platform.cross }}
|
||||
run: cargo ${{ matrix.platform.command }} --target ${{ matrix.platform.target }} ${{ matrix.features.args }} --manifest-path crates/tauri/Cargo.toml
|
||||
# Using --lib --bins --tests to skip doc tests
|
||||
run: cargo ${{ matrix.platform.command }} --target ${{ matrix.platform.target }} ${{ matrix.features.args }} --lib --bins --tests --manifest-path crates/tauri-utils/Cargo.toml
|
||||
|
||||
- name: test (using cross)
|
||||
- name: test tauri-utils (using cross)
|
||||
if: ${{ matrix.platform.cross }}
|
||||
# Using --lib --bins --tests to skip doc tests
|
||||
run: |
|
||||
cargo install cross --git https://github.com/cross-rs/cross --rev 51f46f296253d8122c927c5bb933e3c4f27cc317 --locked
|
||||
cross ${{ matrix.platform.command }} --target ${{ matrix.platform.target }} ${{ matrix.features.args }} --lib --bins --tests --manifest-path crates/tauri-utils/Cargo.toml
|
||||
|
||||
- name: test tauri
|
||||
if: ${{ !matrix.platform.cross }}
|
||||
run: cargo ${{ matrix.features.key == 'no-default' && 'check' || matrix.platform.command }} --target ${{ matrix.platform.target }} ${{ matrix.features.args }} --manifest-path crates/tauri/Cargo.toml
|
||||
|
||||
- name: test tauri (using cross)
|
||||
if: ${{ matrix.platform.cross }}
|
||||
run: |
|
||||
cargo install cross --git https://github.com/cross-rs/cross --rev ac4c11cedc97cd7c27faed36e55377a90e6ed618 --locked
|
||||
cross ${{ matrix.platform.command }} --target ${{ matrix.platform.target }} ${{ matrix.features.args }} --manifest-path crates/tauri/Cargo.toml
|
||||
cargo install cross --git https://github.com/cross-rs/cross --rev 51f46f296253d8122c927c5bb933e3c4f27cc317 --locked
|
||||
cross ${{ matrix.features.key == 'no-default' && 'check' || matrix.platform.command }} --target ${{ matrix.platform.target }} ${{ matrix.features.args }} --manifest-path crates/tauri/Cargo.toml
|
||||
|
||||
107
.gitignore
vendored
107
.gitignore
vendored
@@ -1,52 +1,55 @@
|
||||
# dependency directories
|
||||
node_modules/
|
||||
|
||||
# Optional npm and yarn cache directory
|
||||
.npm/
|
||||
.yarn/
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# .vscode workspace settings file
|
||||
.vscode/settings.json
|
||||
|
||||
# npm, yarn and bun lock files
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
bun.lockb
|
||||
|
||||
# rust compiled folders
|
||||
target/
|
||||
|
||||
# test video for streaming example
|
||||
streaming_example_test_video.mp4
|
||||
|
||||
# examples /gen directory
|
||||
/examples/**/src-tauri/gen/
|
||||
/bench/**/src-tauri/gen/
|
||||
|
||||
# logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# miscellaneous
|
||||
/.vs
|
||||
.DS_Store
|
||||
.Thumbs.db
|
||||
*.sublime*
|
||||
.idea
|
||||
debug.log
|
||||
TODO.md
|
||||
# dependency directories
|
||||
node_modules/
|
||||
|
||||
# Optional npm and yarn cache directory
|
||||
.npm/
|
||||
.yarn/
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# .vscode workspace settings file
|
||||
.vscode/settings.json
|
||||
.vscode/launch.json
|
||||
.vscode/tasks.json
|
||||
|
||||
# npm, yarn and bun lock files
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
bun.lockb
|
||||
|
||||
# rust compiled folders
|
||||
target/
|
||||
|
||||
# test video for streaming example
|
||||
streaming_example_test_video.mp4
|
||||
|
||||
# examples /gen directory
|
||||
/examples/**/src-tauri/gen/
|
||||
/bench/**/src-tauri/gen/
|
||||
|
||||
# logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# miscellaneous
|
||||
/.vs
|
||||
.DS_Store
|
||||
.Thumbs.db
|
||||
*.sublime*
|
||||
.idea
|
||||
debug.log
|
||||
TODO.md
|
||||
.aider*
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"semi": false,
|
||||
"trailingComma": "none"
|
||||
"trailingComma": "none",
|
||||
"experimentalOperatorPosition": "start"
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ function checkChangeFiles(changeFiles) {
|
||||
for (const [file, packages] of unknownTagsEntries) {
|
||||
for (const { package, tag } of packages) {
|
||||
console.error(
|
||||
`Package \`${package}\` has an uknown change tag ${tag} in ${file} `
|
||||
`Package \`${package}\` has an unknown change tag ${tag} in ${file} `
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ const ignore = [
|
||||
|
||||
async function checkFile(file) {
|
||||
if (
|
||||
extensions.some((e) => file.endsWith(e)) &&
|
||||
!ignore.some((i) => file.includes(`/${i}/`) || path.basename(file) == i)
|
||||
extensions.some((e) => file.endsWith(e))
|
||||
&& !ignore.some((i) => file.includes(`/${i}/`) || path.basename(file) === i)
|
||||
) {
|
||||
const fileStream = fs.createReadStream(file)
|
||||
const rl = readline.createInterface({
|
||||
@@ -42,11 +42,11 @@ async function checkFile(file) {
|
||||
for await (let line of rl) {
|
||||
// ignore empty lines, allow shebang and bundler license
|
||||
if (
|
||||
line.length === 0 ||
|
||||
line.startsWith('#!') ||
|
||||
line.startsWith('// swift-tools-version:') ||
|
||||
line === bundlerLicense ||
|
||||
line === denoLicense
|
||||
line.length === 0
|
||||
|| line.startsWith('#!')
|
||||
|| line.startsWith('// swift-tools-version:')
|
||||
|| line === bundlerLicense
|
||||
|| line === denoLicense
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
16
.taurignore
16
.taurignore
@@ -1,16 +0,0 @@
|
||||
.changes
|
||||
.devcontainer
|
||||
.docker
|
||||
.github
|
||||
.scripts
|
||||
.vscode
|
||||
audits
|
||||
bench
|
||||
packages/api
|
||||
packages/cli
|
||||
crates/tauri-cli
|
||||
crates/tauri-bundler
|
||||
crates/tauri-driver
|
||||
crates/tauri-macos-sign
|
||||
crates/tauri-schema-generator
|
||||
crates/tests
|
||||
5056
Cargo.lock
generated
5056
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -43,7 +43,7 @@ repository = "https://github.com/tauri-apps/tauri"
|
||||
categories = ["gui", "web-programming"]
|
||||
license = "Apache-2.0 OR MIT"
|
||||
edition = "2021"
|
||||
rust-version = "1.78"
|
||||
rust-version = "1.77.2"
|
||||
|
||||
# default to small, optimized workspace release binaries
|
||||
[profile.release]
|
||||
@@ -70,3 +70,5 @@ opt-level = "s"
|
||||
[patch.crates-io]
|
||||
schemars_derive = { git = 'https://github.com/tauri-apps/schemars.git', branch = 'feat/preserve-description-newlines' }
|
||||
tauri = { path = "./crates/tauri" }
|
||||
tauri-plugin = { path = "./crates/tauri-plugin" }
|
||||
tauri-utils = { path = "./crates/tauri-utils" }
|
||||
|
||||
@@ -6,7 +6,7 @@ PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy
|
||||
PackageHomePage: https://tauri.app
|
||||
PackageLicenseDeclared: Apache-2.0
|
||||
PackageLicenseDeclared: MIT
|
||||
PackageCopyrightText: 2019-2024, The Tauri Programme in the Commons Conservancy
|
||||
PackageCopyrightText: 2019-2025, The Tauri Programme in the Commons Conservancy
|
||||
PackageSummary: <text>Tauri is a rust project that enables developers to make secure
|
||||
and small desktop applications using a web frontend.
|
||||
</text>
|
||||
|
||||
20
README.md
20
README.md
@@ -4,7 +4,7 @@
|
||||
[](https://opencollective.com/tauri)
|
||||
[](https://github.com/tauri-apps/tauri/actions/workflows/test-core.yml)
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Ftauri-apps%2Ftauri?ref=badge_shield)
|
||||
[](https://discord.gg/SpmNs4S)
|
||||
[](https://discord.com/invite/tauri)
|
||||
[](https://tauri.app)
|
||||
[](https://good-labs.github.io/greater-good-affirmation)
|
||||
[](https://opencollective.com/tauri)
|
||||
@@ -35,7 +35,7 @@ The list of Tauri's features includes, but is not limited to:
|
||||
- Built-in self updater (desktop only)
|
||||
- System tray icons
|
||||
- Native notifications
|
||||
- [Localhost free (🔥)](https://github.com/tauri-apps/tauri/issues/10510)
|
||||
- Native WebView Protocol (tauri doesn't create a localhost http(s) server to serve the WebView contents)
|
||||
- GitHub action for streamlined CI
|
||||
- VS Code extension
|
||||
|
||||
@@ -43,13 +43,13 @@ The list of Tauri's features includes, but is not limited to:
|
||||
|
||||
Tauri currently supports development and distribution on the following platforms:
|
||||
|
||||
| Platform | Versions |
|
||||
| :---------------- | :-------------------------------------------------------------------------------------------------------------- |
|
||||
| Windows | 7 and above |
|
||||
| macOS | 10.15 and above |
|
||||
| Linux | webkit2gtk 4.0 for Tauri v1 (for example Ubuntu 18.04). webkit2gtk 4.1 for Tauri v2 (for example Ubuntu 22.04). |
|
||||
| iOS/iPadOS (beta) | 9 and above |
|
||||
| Android (beta) | 7 and above |
|
||||
| Platform | Versions |
|
||||
| :--------- | :-------------------------------------------------------------------------------------------------------------- |
|
||||
| Windows | 7 and above |
|
||||
| macOS | 10.15 and above |
|
||||
| Linux | webkit2gtk 4.0 for Tauri v1 (for example Ubuntu 18.04). webkit2gtk 4.1 for Tauri v2 (for example Ubuntu 22.04). |
|
||||
| iOS/iPadOS | 9 and above |
|
||||
| Android | 7 and above (currently 8 and above) |
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -81,7 +81,7 @@ For the complete list of sponsors please visit our [website](https://tauri.app#s
|
||||
|
||||
## Organization
|
||||
|
||||
Tauri aims to be a sustainable collective based on principles that guide [sustainable free and open software communities](https://sfosc.org). To this end it has become a Programme within the [Commons Conservancy](https://commonsconservancy.org/), and you can contribute financially via [Open Collective](https://opencollective.com/tauri).
|
||||
Tauri aims to be a sustainable collective based on principles that guide sustainable free and open software communities. To this end it has become a Programme within the [Commons Conservancy](https://commonsconservancy.org/), and you can contribute financially via [Open Collective](https://opencollective.com/tauri).
|
||||
|
||||
## Licenses
|
||||
|
||||
|
||||
@@ -3,17 +3,17 @@ name = "tauri_bench"
|
||||
version = "0.1.0"
|
||||
authors = ["Tauri Programme within The Commons Conservancy"]
|
||||
edition = "2021"
|
||||
rust-version = "1.78"
|
||||
rust-version = "1.77.2"
|
||||
license = "Apache-2.0 OR MIT"
|
||||
description = "Cross-platform WebView rendering library"
|
||||
repository = "https://github.com/tauri-apps/wry"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.40"
|
||||
anyhow = "1"
|
||||
time = { version = "0.3", features = ["formatting"] }
|
||||
tempfile = "3.2.0"
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tempfile = "3"
|
||||
serde_json = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
[[bin]]
|
||||
name = "run_benchmark"
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! [](https://tauri.app)
|
||||
//!
|
||||
//! This Rust binary runs on CI and provides internal metrics results of Tauri. To learn more see [benchmark_results](https://github.com/tauri-apps/benchmark_results) repository.
|
||||
//!
|
||||
//! ***_Internal use only_**
|
||||
@@ -12,6 +10,8 @@
|
||||
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png",
|
||||
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png"
|
||||
)]
|
||||
// file is used by multiple binaries
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::{fs::File, io::BufReader};
|
||||
mod utils;
|
||||
@@ -54,7 +54,7 @@ fn main() {
|
||||
.expect("Something wrong with tauri_data"),
|
||||
&serde_json::to_value(all_data).expect("Unable to build final json (all)"),
|
||||
)
|
||||
.unwrap_or_else(|_| panic!("Unable to write {:?}", tauri_data));
|
||||
.unwrap_or_else(|_| panic!("Unable to write {tauri_data:?}"));
|
||||
|
||||
utils::write_json(
|
||||
tauri_recent
|
||||
@@ -62,5 +62,5 @@ fn main() {
|
||||
.expect("Something wrong with tauri_recent"),
|
||||
&serde_json::to_value(recent).expect("Unable to build final json (recent)"),
|
||||
)
|
||||
.unwrap_or_else(|_| panic!("Unable to write {:?}", tauri_recent));
|
||||
.unwrap_or_else(|_| panic!("Unable to write {tauri_recent:?}"));
|
||||
}
|
||||
|
||||
@@ -2,18 +2,17 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! [](https://tauri.app)
|
||||
//! This Rust binary runs on CI and provides internal metrics results of Tauri.
|
||||
//! To learn more see [benchmark_results](https://github.com/tauri-apps/benchmark_results) repository.
|
||||
//!
|
||||
//! This Rust binary runs on CI and provides internal metrics results of Tauri. To learn more see [benchmark_results](https://github.com/tauri-apps/benchmark_results) repository.
|
||||
//!
|
||||
//! ***_Internal use only_**
|
||||
//! ***_Internal use only_***
|
||||
|
||||
#![doc(
|
||||
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png",
|
||||
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png"
|
||||
)]
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
env,
|
||||
@@ -23,62 +22,66 @@ use std::{
|
||||
|
||||
mod utils;
|
||||
|
||||
/// The list of the examples of the benchmark name and binary relative path
|
||||
fn get_all_benchmarks() -> Vec<(String, String)> {
|
||||
/// The list of examples for benchmarks
|
||||
fn get_all_benchmarks(target: &str) -> Vec<(String, String)> {
|
||||
vec![
|
||||
(
|
||||
"tauri_hello_world".into(),
|
||||
format!("../target/{}/release/bench_helloworld", utils::get_target()),
|
||||
format!("../target/{target}/release/bench_helloworld"),
|
||||
),
|
||||
(
|
||||
"tauri_cpu_intensive".into(),
|
||||
format!(
|
||||
"../target/{}/release/bench_cpu_intensive",
|
||||
utils::get_target()
|
||||
),
|
||||
format!("../target/{target}/release/bench_cpu_intensive"),
|
||||
),
|
||||
(
|
||||
"tauri_3mb_transfer".into(),
|
||||
format!(
|
||||
"../target/{}/release/bench_files_transfer",
|
||||
utils::get_target()
|
||||
),
|
||||
format!("../target/{target}/release/bench_files_transfer"),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn run_strace_benchmarks(new_data: &mut utils::BenchResult) -> Result<()> {
|
||||
fn run_strace_benchmarks(new_data: &mut utils::BenchResult, target: &str) -> Result<()> {
|
||||
use std::io::Read;
|
||||
|
||||
let mut thread_count = HashMap::<String, u64>::new();
|
||||
let mut syscall_count = HashMap::<String, u64>::new();
|
||||
|
||||
for (name, example_exe) in get_all_benchmarks() {
|
||||
let mut file = tempfile::NamedTempFile::new()?;
|
||||
for (name, example_exe) in get_all_benchmarks(target) {
|
||||
let mut file = tempfile::NamedTempFile::new()
|
||||
.context("failed to create temporary file for strace output")?;
|
||||
|
||||
let exe_path = utils::bench_root_path().join(&example_exe);
|
||||
let exe_path_str = exe_path
|
||||
.to_str()
|
||||
.context("executable path contains invalid UTF-8")?;
|
||||
let temp_path_str = file
|
||||
.path()
|
||||
.to_str()
|
||||
.context("temporary file path contains invalid UTF-8")?;
|
||||
|
||||
Command::new("strace")
|
||||
.args([
|
||||
"-c",
|
||||
"-f",
|
||||
"-o",
|
||||
file.path().to_str().unwrap(),
|
||||
utils::bench_root_path().join(example_exe).to_str().unwrap(),
|
||||
])
|
||||
.args(["-c", "-f", "-o", temp_path_str, exe_path_str])
|
||||
.stdout(Stdio::inherit())
|
||||
.spawn()?
|
||||
.wait()?;
|
||||
.spawn()
|
||||
.context("failed to spawn strace process")?
|
||||
.wait()
|
||||
.context("failed to wait for strace process")?;
|
||||
|
||||
let mut output = String::new();
|
||||
file.as_file_mut().read_to_string(&mut output)?;
|
||||
file
|
||||
.as_file_mut()
|
||||
.read_to_string(&mut output)
|
||||
.context("failed to read strace output")?;
|
||||
|
||||
let strace_result = utils::parse_strace_output(&output);
|
||||
// Note, we always have 1 thread. Use cloneX calls as counter for additional threads created.
|
||||
let clone = 1
|
||||
+ strace_result.get("clone").map(|d| d.calls).unwrap_or(0)
|
||||
// Count clone/clone3 syscalls as thread creation indicators
|
||||
let clone_calls = strace_result.get("clone").map(|d| d.calls).unwrap_or(0)
|
||||
+ strace_result.get("clone3").map(|d| d.calls).unwrap_or(0);
|
||||
let total = strace_result.get("total").unwrap().calls;
|
||||
thread_count.insert(name.to_string(), clone);
|
||||
syscall_count.insert(name.to_string(), total);
|
||||
|
||||
if let Some(total) = strace_result.get("total") {
|
||||
thread_count.insert(name.clone(), clone_calls);
|
||||
syscall_count.insert(name, total.calls);
|
||||
}
|
||||
}
|
||||
|
||||
new_data.thread_count = thread_count;
|
||||
@@ -87,70 +90,100 @@ fn run_strace_benchmarks(new_data: &mut utils::BenchResult) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_max_mem_benchmark() -> Result<HashMap<String, u64>> {
|
||||
fn run_max_mem_benchmark(target: &str) -> Result<HashMap<String, u64>> {
|
||||
let mut results = HashMap::<String, u64>::new();
|
||||
|
||||
for (name, example_exe) in get_all_benchmarks() {
|
||||
let benchmark_file = utils::target_dir().join(format!("mprof{}_.dat", name));
|
||||
let benchmark_file = benchmark_file.to_str().unwrap();
|
||||
for (name, example_exe) in get_all_benchmarks(target) {
|
||||
let benchmark_file = utils::target_dir().join(format!("mprof{name}_.dat"));
|
||||
let benchmark_file_str = benchmark_file
|
||||
.to_str()
|
||||
.context("benchmark file path contains invalid UTF-8")?;
|
||||
|
||||
let exe_path = utils::bench_root_path().join(&example_exe);
|
||||
let exe_path_str = exe_path
|
||||
.to_str()
|
||||
.context("executable path contains invalid UTF-8")?;
|
||||
|
||||
let proc = Command::new("mprof")
|
||||
.args([
|
||||
"run",
|
||||
"-C",
|
||||
"-o",
|
||||
benchmark_file,
|
||||
utils::bench_root_path().join(example_exe).to_str().unwrap(),
|
||||
])
|
||||
.args(["run", "-C", "-o", benchmark_file_str, exe_path_str])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
.spawn()
|
||||
.with_context(|| format!("failed to spawn mprof for benchmark {name}"))?;
|
||||
|
||||
let proc_result = proc.wait_with_output()?;
|
||||
println!("{:?}", proc_result);
|
||||
results.insert(
|
||||
name.to_string(),
|
||||
utils::parse_max_mem(benchmark_file).unwrap(),
|
||||
);
|
||||
let proc_result = proc
|
||||
.wait_with_output()
|
||||
.with_context(|| format!("failed to wait for mprof {name}"))?;
|
||||
|
||||
if !proc_result.status.success() {
|
||||
eprintln!(
|
||||
"mprof failed for {name}: {}",
|
||||
String::from_utf8_lossy(&proc_result.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(mem) = utils::parse_max_mem(benchmark_file_str)
|
||||
.with_context(|| format!("failed to parse mprof data for {name}"))?
|
||||
{
|
||||
results.insert(name, mem);
|
||||
}
|
||||
|
||||
// Clean up the temporary file
|
||||
if let Err(e) = std::fs::remove_file(&benchmark_file) {
|
||||
eprintln!("Warning: failed to remove temporary file {benchmark_file_str}: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
fn rlib_size(target_dir: &std::path::Path, prefix: &str) -> u64 {
|
||||
fn rlib_size(target_dir: &Path, prefix: &str) -> Result<u64> {
|
||||
let mut size = 0;
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
let deps_dir = target_dir.join("deps");
|
||||
for entry in std::fs::read_dir(&deps_dir).with_context(|| {
|
||||
format!(
|
||||
"failed to read target deps directory: {}",
|
||||
deps_dir.display()
|
||||
)
|
||||
})? {
|
||||
let entry = entry.context("failed to read directory entry")?;
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
|
||||
for entry in std::fs::read_dir(target_dir.join("deps")).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let os_str = entry.file_name();
|
||||
let name = os_str.to_str().unwrap();
|
||||
if name.starts_with(prefix) && name.ends_with(".rlib") {
|
||||
let start = name.split('-').next().unwrap().to_string();
|
||||
if seen.contains(&start) {
|
||||
println!("skip {}", name);
|
||||
} else {
|
||||
seen.insert(start);
|
||||
size += entry.metadata().unwrap().len();
|
||||
println!("check size {} {}", name, size);
|
||||
if let Some(start) = name.split('-').next() {
|
||||
if seen.insert(start.to_string()) {
|
||||
size += entry
|
||||
.metadata()
|
||||
.context("failed to read file metadata")?
|
||||
.len();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(size > 0);
|
||||
size
|
||||
|
||||
if size == 0 {
|
||||
anyhow::bail!(
|
||||
"no rlib files found for prefix {prefix} in {}",
|
||||
deps_dir.display()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(size)
|
||||
}
|
||||
|
||||
fn get_binary_sizes(target_dir: &Path) -> Result<HashMap<String, u64>> {
|
||||
fn get_binary_sizes(target_dir: &Path, target: &str) -> Result<HashMap<String, u64>> {
|
||||
let mut sizes = HashMap::<String, u64>::new();
|
||||
|
||||
let wry_size = rlib_size(target_dir, "libwry");
|
||||
println!("wry {} bytes", wry_size);
|
||||
let wry_size = rlib_size(target_dir, "libwry")?;
|
||||
sizes.insert("wry_rlib".to_string(), wry_size);
|
||||
|
||||
// add size for all EXEC_TIME_BENCHMARKS
|
||||
for (name, example_exe) in get_all_benchmarks() {
|
||||
let meta = std::fs::metadata(example_exe).unwrap();
|
||||
sizes.insert(name.to_string(), meta.len());
|
||||
for (name, example_exe) in get_all_benchmarks(target) {
|
||||
let exe_path = utils::bench_root_path().join(&example_exe);
|
||||
let meta = std::fs::metadata(&exe_path)
|
||||
.with_context(|| format!("failed to read metadata for {}", exe_path.display()))?;
|
||||
sizes.insert(name, meta.len());
|
||||
}
|
||||
|
||||
Ok(sizes)
|
||||
@@ -190,14 +223,33 @@ fn cargo_deps() -> HashMap<String, usize> {
|
||||
cmd.args(["--target", target]);
|
||||
cmd.current_dir(utils::tauri_root_path());
|
||||
|
||||
let full_deps = cmd.output().expect("failed to run cargo tree").stdout;
|
||||
let full_deps = String::from_utf8(full_deps).expect("cargo tree output not utf-8");
|
||||
let count = full_deps.lines().collect::<HashSet<_>>().len() - 1; // output includes wry itself
|
||||
match cmd.output() {
|
||||
Ok(output) if output.status.success() => {
|
||||
let full_deps = String::from_utf8_lossy(&output.stdout);
|
||||
let count = full_deps
|
||||
.lines()
|
||||
.collect::<HashSet<_>>()
|
||||
.len()
|
||||
.saturating_sub(1); // output includes wry itself
|
||||
|
||||
// set the count to the highest count seen for this OS
|
||||
let existing = results.entry(os.to_string()).or_default();
|
||||
*existing = count.max(*existing);
|
||||
assert!(count > 10); // sanity check
|
||||
// set the count to the highest count seen for this OS
|
||||
let existing = results.entry(os.to_string()).or_default();
|
||||
*existing = count.max(*existing);
|
||||
|
||||
if count <= 10 {
|
||||
eprintln!("Warning: dependency count for {target} seems low: {count}");
|
||||
}
|
||||
}
|
||||
Ok(output) => {
|
||||
eprintln!(
|
||||
"cargo tree failed for {target}: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to run cargo tree for {target}: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
results
|
||||
@@ -205,104 +257,127 @@ fn cargo_deps() -> HashMap<String, usize> {
|
||||
|
||||
const RESULT_KEYS: &[&str] = &["mean", "stddev", "user", "system", "min", "max"];
|
||||
|
||||
fn run_exec_time(target_dir: &Path) -> Result<HashMap<String, HashMap<String, f64>>> {
|
||||
fn run_exec_time(target: &str) -> Result<HashMap<String, HashMap<String, f64>>> {
|
||||
let target_dir = utils::target_dir();
|
||||
let benchmark_file = target_dir.join("hyperfine_results.json");
|
||||
let benchmark_file = benchmark_file.to_str().unwrap();
|
||||
let benchmark_file_str = benchmark_file
|
||||
.to_str()
|
||||
.context("benchmark file path contains invalid UTF-8")?;
|
||||
|
||||
let mut command = [
|
||||
let mut command = vec![
|
||||
"hyperfine",
|
||||
"--export-json",
|
||||
benchmark_file,
|
||||
benchmark_file_str,
|
||||
"--show-output",
|
||||
"--warmup",
|
||||
"3",
|
||||
]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
];
|
||||
|
||||
for (_, example_exe) in get_all_benchmarks() {
|
||||
command.push(
|
||||
utils::bench_root_path()
|
||||
.join(example_exe)
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
);
|
||||
let benchmarks = get_all_benchmarks(target);
|
||||
let mut benchmark_paths = Vec::new();
|
||||
|
||||
for (_, example_exe) in &benchmarks {
|
||||
let exe_path = utils::bench_root_path().join(example_exe);
|
||||
let exe_path_str = exe_path
|
||||
.to_str()
|
||||
.context("executable path contains invalid UTF-8")?;
|
||||
benchmark_paths.push(exe_path_str.to_string());
|
||||
}
|
||||
|
||||
utils::run(&command.iter().map(|s| s.as_ref()).collect::<Vec<_>>());
|
||||
for path in &benchmark_paths {
|
||||
command.push(path.as_str());
|
||||
}
|
||||
|
||||
utils::run(&command)?;
|
||||
|
||||
let mut results = HashMap::<String, HashMap<String, f64>>::new();
|
||||
let hyperfine_results = utils::read_json(benchmark_file)?;
|
||||
for ((name, _), data) in get_all_benchmarks().iter().zip(
|
||||
hyperfine_results
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.get("results")
|
||||
.unwrap()
|
||||
.as_array()
|
||||
.unwrap(),
|
||||
) {
|
||||
let data = data.as_object().unwrap().clone();
|
||||
results.insert(
|
||||
name.to_string(),
|
||||
data
|
||||
.into_iter()
|
||||
.filter(|(key, _)| RESULT_KEYS.contains(&key.as_str()))
|
||||
.map(|(key, val)| (key, val.as_f64().unwrap()))
|
||||
.collect(),
|
||||
);
|
||||
let hyperfine_results = utils::read_json(benchmark_file_str)?;
|
||||
|
||||
if let Some(results_array) = hyperfine_results
|
||||
.as_object()
|
||||
.and_then(|obj| obj.get("results"))
|
||||
.and_then(|val| val.as_array())
|
||||
{
|
||||
for ((name, _), data) in benchmarks.iter().zip(results_array.iter()) {
|
||||
if let Some(data_obj) = data.as_object() {
|
||||
let filtered_data: HashMap<String, f64> = data_obj
|
||||
.iter()
|
||||
.filter(|(key, _)| RESULT_KEYS.contains(&key.as_str()))
|
||||
.filter_map(|(key, val)| val.as_f64().map(|v| (key.clone(), v)))
|
||||
.collect();
|
||||
|
||||
results.insert(name.clone(), filtered_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// download big files if not present
|
||||
let json_3mb = utils::home_path().join(".tauri_3mb.json");
|
||||
|
||||
if !json_3mb.exists() {
|
||||
println!("Downloading test data...");
|
||||
utils::download_file(
|
||||
"https://github.com/lemarier/tauri-test/releases/download/v2.0.0/json_3mb.json",
|
||||
json_3mb,
|
||||
);
|
||||
)
|
||||
.context("failed to download test data")?;
|
||||
}
|
||||
|
||||
println!("Starting tauri benchmark");
|
||||
|
||||
let target_dir = utils::target_dir();
|
||||
let target = utils::get_target();
|
||||
|
||||
env::set_current_dir(utils::bench_root_path())?;
|
||||
env::set_current_dir(utils::bench_root_path())
|
||||
.context("failed to set working directory to bench root")?;
|
||||
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.context("failed to get current time")?;
|
||||
let timestamp = format!("{}", now.as_secs());
|
||||
|
||||
println!("Running execution time benchmarks...");
|
||||
let exec_time = run_exec_time(target)?;
|
||||
|
||||
println!("Getting binary sizes...");
|
||||
let binary_size = get_binary_sizes(&target_dir, target)?;
|
||||
|
||||
println!("Analyzing cargo dependencies...");
|
||||
let cargo_deps = cargo_deps();
|
||||
|
||||
let format =
|
||||
time::format_description::parse("[year]-[month]-[day]T[hour]:[minute]:[second]Z").unwrap();
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
let mut new_data = utils::BenchResult {
|
||||
created_at: now.format(&format).unwrap(),
|
||||
sha1: utils::run_collect(&["git", "rev-parse", "HEAD"])
|
||||
.0
|
||||
.trim()
|
||||
.to_string(),
|
||||
exec_time: run_exec_time(&target_dir)?,
|
||||
binary_size: get_binary_sizes(&target_dir)?,
|
||||
cargo_deps: cargo_deps(),
|
||||
created_at: timestamp,
|
||||
sha1: {
|
||||
let output = utils::run_collect(&["git", "rev-parse", "HEAD"])?;
|
||||
output.0.trim().to_string()
|
||||
},
|
||||
exec_time,
|
||||
binary_size,
|
||||
cargo_deps,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if cfg!(target_os = "linux") {
|
||||
run_strace_benchmarks(&mut new_data)?;
|
||||
new_data.max_memory = run_max_mem_benchmark()?;
|
||||
println!("Running Linux-specific benchmarks...");
|
||||
run_strace_benchmarks(&mut new_data, target)?;
|
||||
new_data.max_memory = run_max_mem_benchmark(target)?;
|
||||
}
|
||||
|
||||
println!("===== <BENCHMARK RESULTS>");
|
||||
serde_json::to_writer_pretty(std::io::stdout(), &new_data)?;
|
||||
serde_json::to_writer_pretty(std::io::stdout(), &new_data)
|
||||
.context("failed to serialize benchmark results")?;
|
||||
println!("\n===== </BENCHMARK RESULTS>");
|
||||
|
||||
if let Some(filename) = target_dir.join("bench.json").to_str() {
|
||||
utils::write_json(filename, &serde_json::to_value(&new_data)?)?;
|
||||
let bench_file = target_dir.join("bench.json");
|
||||
if let Some(filename) = bench_file.to_str() {
|
||||
utils::write_json(filename, &serde_json::to_value(&new_data)?)
|
||||
.context("failed to write benchmark results to file")?;
|
||||
println!("Results written to: {filename}");
|
||||
} else {
|
||||
eprintln!("Cannot write bench.json, path is invalid");
|
||||
eprintln!("Cannot write bench.json, path contains invalid UTF-8");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -2,7 +2,16 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use anyhow::Result;
|
||||
//! Utility functions for benchmarking tasks in the Tauri project.
|
||||
//!
|
||||
//! This module provides helpers for:
|
||||
//! - Paths to project directories and targets
|
||||
//! - Running and collecting process outputs
|
||||
//! - Parsing memory profiler (`mprof`) and syscall profiler (`strace`) outputs
|
||||
//! - JSON read/write utilities
|
||||
//! - File download utilities (via `curl` or file copy)
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
@@ -13,6 +22,7 @@ use std::{
|
||||
process::{Command, Output, Stdio},
|
||||
};
|
||||
|
||||
/// Holds the results of a benchmark run.
|
||||
#[derive(Default, Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct BenchResult {
|
||||
pub created_at: String,
|
||||
@@ -25,7 +35,7 @@ pub struct BenchResult {
|
||||
pub cargo_deps: HashMap<String, usize>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Represents a single line of parsed `strace` output.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct StraceOutput {
|
||||
pub percent_time: f64,
|
||||
@@ -35,6 +45,7 @@ pub struct StraceOutput {
|
||||
pub errors: u64,
|
||||
}
|
||||
|
||||
/// Get the compilation target triple for the current platform.
|
||||
pub fn get_target() -> &'static str {
|
||||
#[cfg(target_os = "macos")]
|
||||
return if cfg!(target_arch = "aarch64") {
|
||||
@@ -42,18 +53,22 @@ pub fn get_target() -> &'static str {
|
||||
} else {
|
||||
"x86_64-apple-darwin"
|
||||
};
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
return if cfg!(target_arch = "aarch64") {
|
||||
"aarch64-apple-ios"
|
||||
} else {
|
||||
"x86_64-apple-ios"
|
||||
};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
return "x86_64-unknown-linux-gnu";
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
unimplemented!();
|
||||
unimplemented!("Windows target not implemented yet");
|
||||
}
|
||||
|
||||
/// Get the `target/release` directory path for benchmarks.
|
||||
pub fn target_dir() -> PathBuf {
|
||||
bench_root_path()
|
||||
.join("..")
|
||||
@@ -62,83 +77,90 @@ pub fn target_dir() -> PathBuf {
|
||||
.join("release")
|
||||
}
|
||||
|
||||
/// Get the root path of the current benchmark crate.
|
||||
pub fn bench_root_path() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Get the home directory of the current user.
|
||||
pub fn home_path() -> PathBuf {
|
||||
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "linux"))]
|
||||
return PathBuf::from(env!("HOME"));
|
||||
{
|
||||
PathBuf::from(std::env::var("HOME").unwrap_or_default())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
return PathBuf::from(env!("HOMEPATH"));
|
||||
{
|
||||
PathBuf::from(std::env::var("USERPROFILE").unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Get the root path of the Tauri repository.
|
||||
pub fn tauri_root_path() -> PathBuf {
|
||||
bench_root_path().parent().unwrap().to_path_buf()
|
||||
bench_root_path().parent().map(|p| p.to_path_buf()).unwrap()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn run_collect(cmd: &[&str]) -> (String, String) {
|
||||
let mut process_builder = Command::new(cmd[0]);
|
||||
process_builder
|
||||
/// Run a command and collect its stdout and stderr as strings.
|
||||
/// Returns an error if the command fails or exits with a non-zero status.
|
||||
pub fn run_collect(cmd: &[&str]) -> Result<(String, String)> {
|
||||
let output: Output = Command::new(cmd[0])
|
||||
.args(&cmd[1..])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
let prog = process_builder.spawn().expect("failed to spawn script");
|
||||
let Output {
|
||||
stdout,
|
||||
stderr,
|
||||
status,
|
||||
} = prog.wait_with_output().expect("failed to wait on child");
|
||||
let stdout = String::from_utf8_lossy(&stdout).to_string();
|
||||
let stderr = String::from_utf8_lossy(&stderr).to_string();
|
||||
if !status.success() {
|
||||
eprintln!("stdout: <<<{}>>>", stdout);
|
||||
eprintln!("stderr: <<<{}>>>", stderr);
|
||||
panic!("Unexpected exit code: {:?}", status.code());
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.with_context(|| format!("failed to execute command: {cmd:?}"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
bail!(
|
||||
"Command {:?} exited with {:?}\nstdout:\n{}\nstderr:\n{}",
|
||||
cmd,
|
||||
output.status.code(),
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
(stdout, stderr)
|
||||
|
||||
Ok((
|
||||
String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn parse_max_mem(file_path: &str) -> Option<u64> {
|
||||
let file = fs::File::open(file_path).unwrap();
|
||||
/// Parse a memory profiler (`mprof`) output file and return the maximum
|
||||
/// memory usage in bytes. Returns `None` if no values are found.
|
||||
pub fn parse_max_mem(file_path: &str) -> Result<Option<u64>> {
|
||||
let file = fs::File::open(file_path)
|
||||
.with_context(|| format!("failed to open mprof output file {file_path}"))?;
|
||||
let output = BufReader::new(file);
|
||||
|
||||
let mut highest: u64 = 0;
|
||||
// MEM 203.437500 1621617192.4123
|
||||
|
||||
for line in output.lines().map_while(Result::ok) {
|
||||
// split line by space
|
||||
let split = line.split(' ').collect::<Vec<_>>();
|
||||
let split: Vec<&str> = line.split(' ').collect();
|
||||
if split.len() == 3 {
|
||||
// mprof generate result in MB
|
||||
let current_bytes = str::parse::<f64>(split[1]).unwrap() as u64 * 1024 * 1024;
|
||||
if current_bytes > highest {
|
||||
highest = current_bytes;
|
||||
if let Ok(mb) = split[1].parse::<f64>() {
|
||||
let current_bytes = (mb * 1024.0 * 1024.0) as u64;
|
||||
highest = highest.max(current_bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs::remove_file(file_path).unwrap();
|
||||
// Best-effort cleanup
|
||||
let _ = fs::remove_file(file_path);
|
||||
|
||||
if highest > 0 {
|
||||
return Some(highest);
|
||||
}
|
||||
|
||||
None
|
||||
Ok(if highest > 0 { Some(highest) } else { None })
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Parse the output of `strace -c` and return a summary of syscalls.
|
||||
pub fn parse_strace_output(output: &str) -> HashMap<String, StraceOutput> {
|
||||
let mut summary = HashMap::new();
|
||||
|
||||
let mut lines = output
|
||||
.lines()
|
||||
.filter(|line| !line.is_empty() && !line.contains("detached ..."));
|
||||
let count = lines.clone().count();
|
||||
|
||||
let count = lines.clone().count();
|
||||
if count < 4 {
|
||||
return summary;
|
||||
}
|
||||
@@ -148,89 +170,91 @@ pub fn parse_strace_output(output: &str) -> HashMap<String, StraceOutput> {
|
||||
let data_lines = lines.skip(2);
|
||||
|
||||
for line in data_lines {
|
||||
let syscall_fields = line.split_whitespace().collect::<Vec<_>>();
|
||||
let syscall_fields: Vec<&str> = line.split_whitespace().collect();
|
||||
let len = syscall_fields.len();
|
||||
let syscall_name = syscall_fields.last().unwrap();
|
||||
|
||||
if (5..=6).contains(&len) {
|
||||
summary.insert(
|
||||
syscall_name.to_string(),
|
||||
StraceOutput {
|
||||
percent_time: str::parse::<f64>(syscall_fields[0]).unwrap(),
|
||||
seconds: str::parse::<f64>(syscall_fields[1]).unwrap(),
|
||||
usecs_per_call: Some(str::parse::<u64>(syscall_fields[2]).unwrap()),
|
||||
calls: str::parse::<u64>(syscall_fields[3]).unwrap(),
|
||||
errors: if syscall_fields.len() < 6 {
|
||||
if let Some(&syscall_name) = syscall_fields.last() {
|
||||
if (5..=6).contains(&len) {
|
||||
let output = StraceOutput {
|
||||
percent_time: syscall_fields[0].parse().unwrap_or(0.0),
|
||||
seconds: syscall_fields[1].parse().unwrap_or(0.0),
|
||||
usecs_per_call: syscall_fields[2].parse().ok(),
|
||||
calls: syscall_fields[3].parse().unwrap_or(0),
|
||||
errors: if len < 6 {
|
||||
0
|
||||
} else {
|
||||
str::parse::<u64>(syscall_fields[4]).unwrap()
|
||||
syscall_fields[4].parse().unwrap_or(0)
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
summary.insert(syscall_name.to_string(), output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let total_fields = total_line.split_whitespace().collect::<Vec<_>>();
|
||||
|
||||
summary.insert(
|
||||
"total".to_string(),
|
||||
match total_fields.len() {
|
||||
// Old format, has no usecs/call
|
||||
5 => StraceOutput {
|
||||
percent_time: str::parse::<f64>(total_fields[0]).unwrap(),
|
||||
seconds: str::parse::<f64>(total_fields[1]).unwrap(),
|
||||
usecs_per_call: None,
|
||||
calls: str::parse::<u64>(total_fields[2]).unwrap(),
|
||||
errors: str::parse::<u64>(total_fields[3]).unwrap(),
|
||||
},
|
||||
6 => StraceOutput {
|
||||
percent_time: str::parse::<f64>(total_fields[0]).unwrap(),
|
||||
seconds: str::parse::<f64>(total_fields[1]).unwrap(),
|
||||
usecs_per_call: Some(str::parse::<u64>(total_fields[2]).unwrap()),
|
||||
calls: str::parse::<u64>(total_fields[3]).unwrap(),
|
||||
errors: str::parse::<u64>(total_fields[4]).unwrap(),
|
||||
},
|
||||
_ => panic!("Unexpected total field count: {}", total_fields.len()),
|
||||
let total_fields: Vec<&str> = total_line.split_whitespace().collect();
|
||||
let total = match total_fields.len() {
|
||||
5 => StraceOutput {
|
||||
percent_time: total_fields[0].parse().unwrap_or(0.0),
|
||||
seconds: total_fields[1].parse().unwrap_or(0.0),
|
||||
usecs_per_call: None,
|
||||
calls: total_fields[2].parse().unwrap_or(0),
|
||||
errors: total_fields[3].parse().unwrap_or(0),
|
||||
},
|
||||
);
|
||||
6 => StraceOutput {
|
||||
percent_time: total_fields[0].parse().unwrap_or(0.0),
|
||||
seconds: total_fields[1].parse().unwrap_or(0.0),
|
||||
usecs_per_call: total_fields[2].parse().ok(),
|
||||
calls: total_fields[3].parse().unwrap_or(0),
|
||||
errors: total_fields[4].parse().unwrap_or(0),
|
||||
},
|
||||
_ => {
|
||||
panic!("Unexpected total field count: {}", total_fields.len());
|
||||
}
|
||||
};
|
||||
|
||||
summary.insert("total".to_string(), total);
|
||||
summary
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn run(cmd: &[&str]) {
|
||||
let mut process_builder = Command::new(cmd[0]);
|
||||
process_builder.args(&cmd[1..]).stdin(Stdio::piped());
|
||||
let mut prog = process_builder.spawn().expect("failed to spawn script");
|
||||
let status = prog.wait().expect("failed to wait on child");
|
||||
/// Run a command and wait for completion.
|
||||
/// Returns an error if the command fails.
|
||||
pub fn run(cmd: &[&str]) -> Result<()> {
|
||||
let status = Command::new(cmd[0])
|
||||
.args(&cmd[1..])
|
||||
.stdin(Stdio::piped())
|
||||
.status()
|
||||
.with_context(|| format!("failed to execute command: {cmd:?}"))?;
|
||||
|
||||
if !status.success() {
|
||||
panic!("Unexpected exit code: {:?}", status.code());
|
||||
bail!("Command {:?} exited with {:?}", cmd, status.code());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Read a JSON file into a [`serde_json::Value`].
|
||||
pub fn read_json(filename: &str) -> Result<Value> {
|
||||
let f = fs::File::open(filename)?;
|
||||
let f =
|
||||
fs::File::open(filename).with_context(|| format!("failed to open JSON file {filename}"))?;
|
||||
Ok(serde_json::from_reader(f)?)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Write a [`serde_json::Value`] into a JSON file.
|
||||
pub fn write_json(filename: &str, value: &Value) -> Result<()> {
|
||||
let f = fs::File::create(filename)?;
|
||||
let f =
|
||||
fs::File::create(filename).with_context(|| format!("failed to create JSON file {filename}"))?;
|
||||
serde_json::to_writer(f, value)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn download_file(url: &str, filename: PathBuf) {
|
||||
/// Download a file from either a local path or an HTTP/HTTPS URL.
|
||||
/// Falls back to copying the file if the URL does not start with http/https.
|
||||
pub fn download_file(url: &str, filename: PathBuf) -> Result<()> {
|
||||
if !url.starts_with("http:") && !url.starts_with("https:") {
|
||||
fs::copy(url, filename).unwrap();
|
||||
return;
|
||||
fs::copy(url, &filename).with_context(|| format!("failed to copy from {url}"))?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Downloading with curl this saves us from adding
|
||||
// a Rust HTTP client dependency.
|
||||
println!("Downloading {}", url);
|
||||
println!("Downloading {url}");
|
||||
let status = Command::new("curl")
|
||||
.arg("-L")
|
||||
.arg("-s")
|
||||
@@ -238,8 +262,14 @@ pub fn download_file(url: &str, filename: PathBuf) {
|
||||
.arg(&filename)
|
||||
.arg(url)
|
||||
.status()
|
||||
.unwrap();
|
||||
.with_context(|| format!("failed to execute curl for {url}"))?;
|
||||
|
||||
assert!(status.success());
|
||||
assert!(filename.exists());
|
||||
if !status.success() {
|
||||
bail!("curl failed with exit code {:?}", status.code());
|
||||
}
|
||||
if !filename.exists() {
|
||||
bail!("expected file {:?} to exist after download", filename);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica,
|
||||
Arial, sans-serif;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial,
|
||||
sans-serif;
|
||||
margin: auto;
|
||||
max-width: 38rem;
|
||||
padding: 2rem;
|
||||
|
||||
@@ -3,7 +3,7 @@ name = "bench_cpu_intensive"
|
||||
version = "0.1.0"
|
||||
description = "A very simple Tauri Application"
|
||||
edition = "2021"
|
||||
rust-version = "1.78"
|
||||
rust-version = "1.77.2"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { path = "../../../../crates/tauri-build", features = [
|
||||
@@ -11,6 +11,6 @@ tauri-build = { path = "../../../../crates/tauri-build", features = [
|
||||
] }
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
tauri = { path = "../../../../crates/tauri", features = [] }
|
||||
|
||||
@@ -3,7 +3,7 @@ name = "bench_files_transfer"
|
||||
version = "0.1.0"
|
||||
description = "A very simple Tauri Application"
|
||||
edition = "2021"
|
||||
rust-version = "1.78"
|
||||
rust-version = "1.77.2"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { path = "../../../../crates/tauri-build", features = [
|
||||
@@ -11,6 +11,6 @@ tauri-build = { path = "../../../../crates/tauri-build", features = [
|
||||
] }
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
tauri = { path = "../../../../crates/tauri", features = [] }
|
||||
|
||||
@@ -18,7 +18,7 @@ async fn read_file<R: Runtime>(app: AppHandle<R>) -> Result<Response, String> {
|
||||
.path()
|
||||
.resolve(".tauri_3mb.json", BaseDirectory::Home)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let contents = read(&path).map_err(|e| e.to_string())?;
|
||||
let contents = read(path).map_err(|e| e.to_string())?;
|
||||
Ok(Response::new(contents))
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ name = "bench_helloworld"
|
||||
version = "0.1.0"
|
||||
description = "A very simple Tauri Application"
|
||||
edition = "2021"
|
||||
rust-version = "1.78"
|
||||
rust-version = "1.77.2"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { path = "../../../../crates/tauri-build", features = [
|
||||
@@ -11,6 +11,6 @@ tauri-build = { path = "../../../../crates/tauri-build", features = [
|
||||
] }
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
tauri = { path = "../../../../crates/tauri", features = [] }
|
||||
|
||||
@@ -1,5 +1,161 @@
|
||||
# Changelog
|
||||
|
||||
## \[2.5.6]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.8.3`
|
||||
- Upgraded to `tauri-codegen@2.5.5`
|
||||
|
||||
## \[2.5.5]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-codegen@2.5.4`
|
||||
|
||||
## \[2.5.4]
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [`2d28e3143`](https://www.github.com/tauri-apps/tauri/commit/2d28e3143ee3d97d7570ea03877aa00a0d6e47d0) ([#14632](https://www.github.com/tauri-apps/tauri/pull/14632) by [@sftse](https://www.github.com/tauri-apps/tauri/../../sftse)) Small code refactors for improved code readability. No user facing changes.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.8.2`
|
||||
- Upgraded to `tauri-codegen@2.5.3`
|
||||
|
||||
## \[2.5.3]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.8.1`
|
||||
- Upgraded to `tauri-codegen@2.5.2`
|
||||
|
||||
## \[2.5.2]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-codegen@2.5.1`
|
||||
|
||||
## \[2.5.1]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`4b6b8690a`](https://www.github.com/tauri-apps/tauri/commit/4b6b8690ab886ebdf1307951cffbe03e31280baa) ([#14347](https://www.github.com/tauri-apps/tauri/pull/14347) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Fixed an issue that caused docs.rs builds to fail. No user facing changes.
|
||||
|
||||
## \[2.5.0]
|
||||
|
||||
### New Features
|
||||
|
||||
- [`3b4fac201`](https://www.github.com/tauri-apps/tauri/commit/3b4fac2017832d426dd07c5e24e26684eda57f7b) ([#14194](https://www.github.com/tauri-apps/tauri/pull/14194)) Add `tauri.conf.json > bundle > android > autoIncrementVersionCode` config option to automatically increment the Android version code.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.8.0`
|
||||
- Upgraded to `tauri-codegen@2.5.0`
|
||||
|
||||
## \[2.4.1]
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [`c23bec62d`](https://www.github.com/tauri-apps/tauri/commit/c23bec62d6d5724798869681aa1534423aae28e2) ([#14083](https://www.github.com/tauri-apps/tauri/pull/14083) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Tauri now ignores `macOS.minimumSystemVersion` in `tauri dev` to prevent forced rebuilds of macOS specific dependencies when using something like `rust-analyzer` at the same time as `tauri dev`.
|
||||
|
||||
## \[2.4.0]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.7.0`
|
||||
- Upgraded to `tauri-codegen@2.4.0`
|
||||
|
||||
## \[2.3.1]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.6.0`
|
||||
- Upgraded to `tauri-codegen@2.3.1`
|
||||
|
||||
## \[2.2.1]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-codegen@2.3.0`
|
||||
- Upgraded to `tauri-utils@2.5.0`
|
||||
|
||||
## \[2.2.0]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.4.0`
|
||||
- Upgraded to `tauri-codegen@2.2.0`
|
||||
- [`48b12b440`](https://www.github.com/tauri-apps/tauri/commit/48b12b440478937c46fdfef9f9d95194be117020) Update to `tauri-utils@2.4.0`
|
||||
|
||||
## \[2.1.1]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.3.1`
|
||||
- Upgraded to `tauri-codegen@2.1.1`
|
||||
|
||||
## \[2.1.0]
|
||||
|
||||
### New Features
|
||||
|
||||
- [`013f8f652`](https://www.github.com/tauri-apps/tauri/commit/013f8f652302f2d49c5ec0a075582033d8b074fb) ([#12890](https://www.github.com/tauri-apps/tauri/pull/12890) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Added `build > removeUnusedCommands` to trigger the build scripts and macros to remove unused commands based on the capabilities you defined. Note this won't be accounting for dynamically added ACLs so make sure to check it when using this.
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
- [`1cd8f55ee`](https://www.github.com/tauri-apps/tauri/commit/1cd8f55eed326d61860fee62ba2d2f4464bdcfcc) ([#13033](https://www.github.com/tauri-apps/tauri/pull/13033) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Don't ship global `bundle.global.js` if `app > withGlobalTauri` is set to false
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.3.0`
|
||||
- Upgraded to `tauri-codegen@2.1.0`
|
||||
|
||||
## \[2.0.6]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.2.0`
|
||||
- Upgraded to `tauri-codegen@2.0.5`
|
||||
|
||||
## \[2.0.5]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`848d0e060`](https://www.github.com/tauri-apps/tauri/commit/848d0e060e6eb3c8e9e8175adc7896587b5a947d) ([#12270](https://www.github.com/tauri-apps/tauri/pull/12270) by [@aurelj](https://www.github.com/tauri-apps/tauri/../../aurelj)) Update `cargo_toml` to `0.21.0`. This adds compatibility with Rust's 2024 Edition.
|
||||
- [`cd1d026f9`](https://www.github.com/tauri-apps/tauri/commit/cd1d026f9799c26b04acb64f49e7ee0a8b193049) ([#11961](https://www.github.com/tauri-apps/tauri/pull/11961) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix tauri fails to build if the project path contains glob characters
|
||||
|
||||
## \[2.0.4]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.1.1`
|
||||
- Upgraded to `tauri-codegen@2.0.4`
|
||||
|
||||
## \[2.0.3]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.1.0`
|
||||
- Upgraded to `tauri-codegen@2.0.3`
|
||||
|
||||
## \[2.0.2]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.0.2`
|
||||
|
||||
## \[2.0.1]
|
||||
|
||||
### What's Changed
|
||||
|
||||
- [`0ab2b3306`](https://www.github.com/tauri-apps/tauri/commit/0ab2b330644b6419f6cee1d5377bfb5cdda2ccf9) ([#11205](https://www.github.com/tauri-apps/tauri/pull/11205) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.0.1`
|
||||
- Upgraded to `tauri-codegen@2.0.1`
|
||||
|
||||
## \[2.0.0]
|
||||
|
||||
### What's Changed
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tauri-build"
|
||||
version = "2.0.0"
|
||||
version = "2.5.6"
|
||||
description = "build time code to pair with https://crates.io/crates/tauri"
|
||||
exclude = ["CHANGELOG.md", "/target"]
|
||||
readme = "README.md"
|
||||
@@ -22,29 +22,27 @@ targets = [
|
||||
"x86_64-linux-android",
|
||||
"x86_64-apple-ios",
|
||||
]
|
||||
rustc-args = ["--cfg", "docsrs"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
quote = { version = "1", optional = true }
|
||||
tauri-codegen = { version = "2.0.0", path = "../tauri-codegen", optional = true }
|
||||
tauri-utils = { version = "2.0.0", path = "../tauri-utils", features = [
|
||||
"build",
|
||||
tauri-codegen = { version = "2.5.5", path = "../tauri-codegen", optional = true }
|
||||
tauri-utils = { version = "2.8.3", path = "../tauri-utils", features = [
|
||||
"build-2",
|
||||
"resources",
|
||||
] }
|
||||
cargo_toml = "0.17"
|
||||
cargo_toml = "0.22"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
heck = "0.5"
|
||||
json-patch = "2.0"
|
||||
json-patch = "3"
|
||||
walkdir = "2"
|
||||
tauri-winres = "0.1"
|
||||
tauri-winres = "0.3"
|
||||
semver = "1"
|
||||
dirs = "5"
|
||||
dirs = "6"
|
||||
glob = "0.3"
|
||||
toml = "0.8"
|
||||
schemars = { version = "0.8.18", features = ["preserve_order"] }
|
||||
# Our code requires at least 0.8.21 so don't simplify this to 0.8
|
||||
schemars = { version = "0.8.21", features = ["preserve_order"] }
|
||||
|
||||
[features]
|
||||
default = ["config-json"]
|
||||
|
||||
@@ -11,7 +11,9 @@ use std::{
|
||||
use anyhow::{Context, Result};
|
||||
use tauri_utils::{
|
||||
acl::{
|
||||
capability::Capability, manifest::Manifest, schema::CAPABILITIES_SCHEMA_FOLDER_PATH,
|
||||
capability::Capability,
|
||||
manifest::{Manifest, PermissionFile},
|
||||
schema::CAPABILITIES_SCHEMA_FOLDER_PATH,
|
||||
ACL_MANIFESTS_FILE_NAME, APP_ACL_KEY, CAPABILITIES_FILE_NAME,
|
||||
},
|
||||
platform::Target,
|
||||
@@ -155,11 +157,17 @@ fn read_plugins_manifests() -> Result<BTreeMap<String, Manifest>> {
|
||||
Ok(manifests)
|
||||
}
|
||||
|
||||
struct InlinedPluginsAcl {
|
||||
manifests: BTreeMap<String, Manifest>,
|
||||
permission_files: BTreeMap<String, Vec<PermissionFile>>,
|
||||
}
|
||||
|
||||
fn inline_plugins(
|
||||
out_dir: &Path,
|
||||
inlined_plugins: HashMap<&'static str, InlinedPlugin>,
|
||||
) -> Result<BTreeMap<String, Manifest>> {
|
||||
) -> Result<InlinedPluginsAcl> {
|
||||
let mut acl_manifests = BTreeMap::new();
|
||||
let mut permission_files_map = BTreeMap::new();
|
||||
|
||||
for (name, plugin) in inlined_plugins {
|
||||
let plugin_out_dir = out_dir.join("plugins").join(name);
|
||||
@@ -199,7 +207,9 @@ permissions = [{default_permissions}]
|
||||
}
|
||||
|
||||
tauri_utils::acl::build::define_permissions(
|
||||
&plugin_out_dir.join("*").to_string_lossy(),
|
||||
&PathBuf::from(glob::Pattern::escape(&plugin_out_dir.to_string_lossy()))
|
||||
.join("*")
|
||||
.to_string_lossy(),
|
||||
name,
|
||||
&plugin_out_dir,
|
||||
|_| true,
|
||||
@@ -222,28 +232,41 @@ permissions = [{default_permissions}]
|
||||
);
|
||||
}
|
||||
permission_files.extend(tauri_utils::acl::build::define_permissions(
|
||||
&default_permissions_path
|
||||
.join("**")
|
||||
.join("*")
|
||||
.to_string_lossy(),
|
||||
&PathBuf::from(glob::Pattern::escape(
|
||||
&default_permissions_path.to_string_lossy(),
|
||||
))
|
||||
.join("**")
|
||||
.join("*")
|
||||
.to_string_lossy(),
|
||||
name,
|
||||
&plugin_out_dir,
|
||||
|_| true,
|
||||
)?);
|
||||
}
|
||||
|
||||
permission_files_map.insert(name.into(), permission_files.clone());
|
||||
|
||||
let manifest = tauri_utils::acl::manifest::Manifest::new(permission_files, None);
|
||||
acl_manifests.insert(name.into(), manifest);
|
||||
}
|
||||
|
||||
Ok(acl_manifests)
|
||||
Ok(InlinedPluginsAcl {
|
||||
manifests: acl_manifests,
|
||||
permission_files: permission_files_map,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct AppManifestAcl {
|
||||
manifest: Manifest,
|
||||
permission_files: Vec<PermissionFile>,
|
||||
}
|
||||
|
||||
fn app_manifest_permissions(
|
||||
out_dir: &Path,
|
||||
manifest: AppManifest,
|
||||
inlined_plugins: &HashMap<&'static str, InlinedPlugin>,
|
||||
) -> Result<Manifest> {
|
||||
) -> Result<AppManifestAcl> {
|
||||
let app_out_dir = out_dir.join("app-manifest");
|
||||
fs::create_dir_all(&app_out_dir)?;
|
||||
let pkg_name = "__app__";
|
||||
@@ -286,6 +309,7 @@ fn app_manifest_permissions(
|
||||
let inlined_plugins_permissions: Vec<_> = inlined_plugins
|
||||
.keys()
|
||||
.map(|name| permissions_root.join(name))
|
||||
.flat_map(|p| p.canonicalize())
|
||||
.collect();
|
||||
|
||||
permission_files.extend(tauri_utils::acl::build::define_permissions(
|
||||
@@ -304,10 +328,10 @@ fn app_manifest_permissions(
|
||||
)?);
|
||||
}
|
||||
|
||||
Ok(tauri_utils::acl::manifest::Manifest::new(
|
||||
permission_files,
|
||||
None,
|
||||
))
|
||||
Ok(AppManifestAcl {
|
||||
permission_files: permission_files.clone(),
|
||||
manifest: tauri_utils::acl::manifest::Manifest::new(permission_files, None),
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_capabilities(
|
||||
@@ -376,19 +400,21 @@ fn validate_capabilities(
|
||||
pub fn build(out_dir: &Path, target: Target, attributes: &Attributes) -> super::Result<()> {
|
||||
let mut acl_manifests = read_plugins_manifests()?;
|
||||
|
||||
let app_manifest = app_manifest_permissions(
|
||||
let app_acl = app_manifest_permissions(
|
||||
out_dir,
|
||||
attributes.app_manifest,
|
||||
&attributes.inlined_plugins,
|
||||
)?;
|
||||
if app_manifest.default_permission.is_some()
|
||||
|| !app_manifest.permission_sets.is_empty()
|
||||
|| !app_manifest.permissions.is_empty()
|
||||
{
|
||||
acl_manifests.insert(APP_ACL_KEY.into(), app_manifest);
|
||||
let has_app_manifest = app_acl.manifest.default_permission.is_some()
|
||||
|| !app_acl.manifest.permission_sets.is_empty()
|
||||
|| !app_acl.manifest.permissions.is_empty();
|
||||
if has_app_manifest {
|
||||
acl_manifests.insert(APP_ACL_KEY.into(), app_acl.manifest);
|
||||
}
|
||||
|
||||
acl_manifests.extend(inline_plugins(out_dir, attributes.inlined_plugins.clone())?);
|
||||
let inline_plugins_acl = inline_plugins(out_dir, attributes.inlined_plugins.clone())?;
|
||||
|
||||
acl_manifests.extend(inline_plugins_acl.manifests);
|
||||
|
||||
let acl_manifests_path = save_acl_manifests(&acl_manifests)?;
|
||||
fs::copy(acl_manifests_path, out_dir.join(ACL_MANIFESTS_FILE_NAME))?;
|
||||
@@ -406,7 +432,12 @@ pub fn build(out_dir: &Path, target: Target, attributes: &Attributes) -> super::
|
||||
let capabilities_path = save_capabilities(&capabilities)?;
|
||||
fs::copy(capabilities_path, out_dir.join(CAPABILITIES_FILE_NAME))?;
|
||||
|
||||
tauri_utils::plugin::save_global_api_scripts_paths(out_dir);
|
||||
let mut permissions_map = inline_plugins_acl.permission_files;
|
||||
if has_app_manifest {
|
||||
permissions_map.insert(APP_ACL_KEY.to_string(), app_acl.permission_files);
|
||||
}
|
||||
|
||||
tauri_utils::acl::build::generate_allowed_commands(out_dir, Some(capabilities), permissions_map)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -120,6 +120,13 @@ impl CodegenContext {
|
||||
if info_plist_path.exists() {
|
||||
println!("cargo:rerun-if-changed={}", info_plist_path.display());
|
||||
}
|
||||
|
||||
if let Some(plist_path) = &config.bundle.macos.info_plist {
|
||||
let info_plist_path = config_parent.join(plist_path);
|
||||
if info_plist_path.exists() {
|
||||
println!("cargo:rerun-if-changed={}", info_plist_path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let code = context_codegen(ContextData {
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! [](https://tauri.app)
|
||||
//!
|
||||
//! This applies the macros at build-time in order to rig some special features needed by `cargo`.
|
||||
|
||||
#![doc(
|
||||
@@ -59,7 +57,7 @@ fn copy_binaries(
|
||||
binaries: ResourcePaths,
|
||||
target_triple: &str,
|
||||
path: &Path,
|
||||
package_name: Option<&String>,
|
||||
package_name: Option<&str>,
|
||||
) -> Result<()> {
|
||||
for src in binaries {
|
||||
let src = src?;
|
||||
@@ -70,7 +68,7 @@ fn copy_binaries(
|
||||
.to_string_lossy()
|
||||
.replace(&format!("-{target_triple}"), "");
|
||||
|
||||
if package_name.map_or(false, |n| n == &file_name) {
|
||||
if package_name == Some(&file_name) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Cannot define a sidecar with the same name as the Cargo package name `{}`. Please change the sidecar name in the filesystem and the Tauri configuration.",
|
||||
file_name
|
||||
@@ -167,21 +165,21 @@ fn copy_frameworks(dest_dir: &Path, frameworks: &[String]) -> Result<()> {
|
||||
.with_context(|| format!("Failed to create frameworks output directory at {dest_dir:?}"))?;
|
||||
for framework in frameworks.iter() {
|
||||
if framework.ends_with(".framework") {
|
||||
let src_path = PathBuf::from(framework);
|
||||
let src_path = Path::new(framework);
|
||||
let src_name = src_path
|
||||
.file_name()
|
||||
.expect("Couldn't get framework filename");
|
||||
let dest_path = dest_dir.join(src_name);
|
||||
copy_dir(&src_path, &dest_path)?;
|
||||
copy_dir(src_path, &dest_path)?;
|
||||
continue;
|
||||
} else if framework.ends_with(".dylib") {
|
||||
let src_path = PathBuf::from(framework);
|
||||
let src_path = Path::new(framework);
|
||||
if !src_path.exists() {
|
||||
return Err(anyhow::anyhow!("Library not found: {}", framework));
|
||||
}
|
||||
let src_name = src_path.file_name().expect("Couldn't get library filename");
|
||||
let dest_path = dest_dir.join(src_name);
|
||||
copy_file(&src_path, &dest_path)?;
|
||||
copy_file(src_path, &dest_path)?;
|
||||
continue;
|
||||
} else if framework.contains('/') {
|
||||
return Err(anyhow::anyhow!(
|
||||
@@ -194,12 +192,8 @@ fn copy_frameworks(dest_dir: &Path, frameworks: &[String]) -> Result<()> {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if copy_framework_from(&PathBuf::from("/Library/Frameworks/"), framework, dest_dir)?
|
||||
|| copy_framework_from(
|
||||
&PathBuf::from("/Network/Library/Frameworks/"),
|
||||
framework,
|
||||
dest_dir,
|
||||
)?
|
||||
if copy_framework_from("/Library/Frameworks/".as_ref(), framework, dest_dir)?
|
||||
|| copy_framework_from("/Network/Library/Frameworks/".as_ref(), framework, dest_dir)?
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -265,7 +259,7 @@ impl WindowsAttributes {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates the default attriute set wihtou the default app manifest.
|
||||
/// Creates the default attribute set without the default app manifest.
|
||||
#[must_use]
|
||||
pub fn new_without_app_manifest() -> Self {
|
||||
Self {
|
||||
@@ -369,6 +363,8 @@ impl Attributes {
|
||||
|
||||
/// Set the glob pattern to be used to find the capabilities.
|
||||
///
|
||||
/// **WARNING:** The `removeUnusedCommands` option does not work with a custom capabilities path.
|
||||
///
|
||||
/// **Note:** You must emit [rerun-if-changed] instructions for your capabilities directory.
|
||||
///
|
||||
/// [rerun-if-changed]: https://doc.rust-lang.org/cargo/reference/build-scripts.html#rerun-if-changed
|
||||
@@ -415,29 +411,33 @@ impl Attributes {
|
||||
}
|
||||
|
||||
pub fn is_dev() -> bool {
|
||||
env::var("DEP_TAURI_DEV").expect("missing `cargo:dev` instruction, please update tauri to latest")
|
||||
env::var_os("DEP_TAURI_DEV")
|
||||
.expect("missing `cargo:dev` instruction, please update tauri to latest")
|
||||
== "true"
|
||||
}
|
||||
|
||||
/// Run all build time helpers for your Tauri Application.
|
||||
///
|
||||
/// The current helpers include the following:
|
||||
/// * Generates a Windows Resource file when targeting Windows.
|
||||
/// To provide extra configuration, such as [`AppManifest::commands`]
|
||||
/// for fine-grained control over command permissions, see [`try_build`].
|
||||
/// See [`Attributes`] for the complete list of configuration options.
|
||||
///
|
||||
/// # Platforms
|
||||
///
|
||||
/// [`build()`] should be called inside of `build.rs` regardless of the platform:
|
||||
/// * New helpers may target more platforms in the future.
|
||||
/// * Platform specific code is handled by the helpers automatically.
|
||||
/// * A build script is required in order to activate some cargo environmental variables that are
|
||||
/// used when generating code and embedding assets - so [`build()`] may as well be called.
|
||||
/// [`build()`] should be called inside of `build.rs` regardless of the platform, so **DO NOT** use a [conditional compilation]
|
||||
/// check that prevents it from running on any of your targets.
|
||||
///
|
||||
/// In short, this is saying don't put the call to [`build()`] behind a `#[cfg(windows)]`.
|
||||
/// Platform specific code is handled by the helpers automatically.
|
||||
///
|
||||
/// A build script is required in order to activate some cargo environmental variables that are
|
||||
/// used when generating code and embedding assets.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If any of the build time helpers fail, they will [`std::panic!`] with the related error message.
|
||||
/// This is typically desirable when running inside a build script; see [`try_build`] for no panics.
|
||||
///
|
||||
/// [conditional compilation]: https://web.mit.edu/rust-lang_v1.25/arch/amd64_ubuntu1404/share/doc/rust/html/book/first-edition/conditional-compilation.html
|
||||
pub fn build() {
|
||||
if let Err(error) = try_build(Attributes::default()) {
|
||||
let error = format!("{error:#}");
|
||||
@@ -452,20 +452,14 @@ pub fn build() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Non-panicking [`build()`].
|
||||
/// Same as [`build()`], but takes an extra configuration argument, and does not panic.
|
||||
#[allow(unused_variables)]
|
||||
pub fn try_build(attributes: Attributes) -> Result<()> {
|
||||
use anyhow::anyhow;
|
||||
|
||||
println!("cargo:rerun-if-env-changed=TAURI_CONFIG");
|
||||
#[cfg(feature = "config-json")]
|
||||
println!("cargo:rerun-if-changed=tauri.conf.json");
|
||||
#[cfg(feature = "config-json5")]
|
||||
println!("cargo:rerun-if-changed=tauri.conf.json5");
|
||||
#[cfg(feature = "config-toml")]
|
||||
println!("cargo:rerun-if-changed=Tauri.toml");
|
||||
|
||||
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
|
||||
let target_os = env::var_os("CARGO_CFG_TARGET_OS").unwrap();
|
||||
let mobile = target_os == "ios" || target_os == "android";
|
||||
cfg_alias("desktop", !mobile);
|
||||
cfg_alias("mobile", mobile);
|
||||
@@ -473,12 +467,11 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
|
||||
let target_triple = env::var("TARGET").unwrap();
|
||||
let target = tauri_utils::platform::Target::from_triple(&target_triple);
|
||||
|
||||
let (config, merged_config_path) =
|
||||
tauri_utils::config::parse::read_from(target, env::current_dir().unwrap())?;
|
||||
if let Some(merged_config_path) = merged_config_path {
|
||||
println!("cargo:rerun-if-changed={}", merged_config_path.display());
|
||||
let (mut config, config_paths) =
|
||||
tauri_utils::config::parse::read_from(target, &env::current_dir().unwrap())?;
|
||||
for config_file_path in config_paths {
|
||||
println!("cargo:rerun-if-changed={}", config_file_path.display());
|
||||
}
|
||||
let mut config = serde_json::from_value(config)?;
|
||||
if let Ok(env) = env::var("TAURI_CONFIG") {
|
||||
let merge_config: serde_json::Value = serde_json::from_str(&env)?;
|
||||
json_patch::merge(&mut config, &merge_config);
|
||||
@@ -503,31 +496,22 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
|
||||
println!("cargo:rustc-env=TAURI_ANDROID_PACKAGE_NAME_PREFIX={android_package_prefix}");
|
||||
|
||||
if let Some(project_dir) = env::var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) {
|
||||
mobile::generate_gradle_files(project_dir, &config)?;
|
||||
mobile::generate_gradle_files(project_dir)?;
|
||||
}
|
||||
|
||||
cfg_alias("dev", is_dev());
|
||||
|
||||
let ws_path = get_workspace_dir()?;
|
||||
let mut manifest =
|
||||
Manifest::<cargo_toml::Value>::from_slice_with_metadata(&fs::read("Cargo.toml")?)?;
|
||||
let cargo_toml_path = Path::new("Cargo.toml").canonicalize()?;
|
||||
let mut manifest = Manifest::<cargo_toml::Value>::from_path_with_metadata(cargo_toml_path)?;
|
||||
|
||||
if let Ok(ws_manifest) = Manifest::from_path(ws_path.join("Cargo.toml")) {
|
||||
Manifest::complete_from_path_and_workspace(
|
||||
&mut manifest,
|
||||
Path::new("Cargo.toml"),
|
||||
Some((&ws_manifest, ws_path.as_path())),
|
||||
)?;
|
||||
} else {
|
||||
Manifest::complete_from_path(&mut manifest, Path::new("Cargo.toml"))?;
|
||||
}
|
||||
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
|
||||
|
||||
manifest::check(&config, &mut manifest)?;
|
||||
|
||||
acl::build(&out_dir, target, &attributes)?;
|
||||
|
||||
tauri_utils::plugin::save_global_api_scripts_paths(&out_dir, None);
|
||||
|
||||
println!("cargo:rustc-env=TAURI_ENV_TARGET_TRIPLE={target_triple}");
|
||||
// when running codegen in this build script, we need to access the env var directly
|
||||
env::set_var("TAURI_ENV_TARGET_TRIPLE", &target_triple);
|
||||
@@ -543,10 +527,10 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
|
||||
|
||||
if let Some(paths) = &config.bundle.external_bin {
|
||||
copy_binaries(
|
||||
ResourcePaths::new(external_binaries(paths, &target_triple).as_slice(), true),
|
||||
ResourcePaths::new(&external_binaries(paths, &target_triple, &target), true),
|
||||
&target_triple,
|
||||
target_dir,
|
||||
manifest.package.as_ref().map(|p| &p.name),
|
||||
manifest.package.as_ref().map(|p| p.name.as_ref()),
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -555,7 +539,7 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
|
||||
.bundle
|
||||
.resources
|
||||
.clone()
|
||||
.unwrap_or_else(|| BundleResources::List(Vec::new()));
|
||||
.unwrap_or(BundleResources::List(Vec::new()));
|
||||
if target_triple.contains("windows") {
|
||||
if let Some(fixed_webview2_runtime_path) = match &config.bundle.windows.webview_install_mode {
|
||||
WebviewInstallMode::FixedRuntime { path } => Some(path),
|
||||
@@ -586,8 +570,10 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(version) = &config.bundle.macos.minimum_system_version {
|
||||
println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET={version}");
|
||||
if !is_dev() {
|
||||
if let Some(version) = &config.bundle.macos.minimum_system_version {
|
||||
println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET={version}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -602,21 +588,19 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
|
||||
use semver::Version;
|
||||
use tauri_winres::{VersionInfo, WindowsResource};
|
||||
|
||||
fn find_icon<F: Fn(&&String) -> bool>(config: &Config, predicate: F, default: &str) -> PathBuf {
|
||||
let icon_path = config
|
||||
.bundle
|
||||
.icon
|
||||
.iter()
|
||||
.find(|i| predicate(i))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| default.to_string());
|
||||
icon_path.into()
|
||||
}
|
||||
|
||||
let window_icon_path = attributes
|
||||
.windows_attributes
|
||||
.window_icon_path
|
||||
.unwrap_or_else(|| find_icon(&config, |i| i.ends_with(".ico"), "icons/icon.ico"));
|
||||
.unwrap_or_else(|| {
|
||||
config
|
||||
.bundle
|
||||
.icon
|
||||
.iter()
|
||||
.find(|i| i.ends_with(".ico"))
|
||||
.map(AsRef::as_ref)
|
||||
.unwrap_or("icons/icon.ico")
|
||||
.into()
|
||||
});
|
||||
|
||||
let mut res = WindowsResource::new();
|
||||
|
||||
@@ -626,7 +610,7 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
|
||||
|
||||
if let Some(version_str) = &config.version {
|
||||
if let Ok(v) = Version::parse(version_str) {
|
||||
let version = v.major << 48 | v.minor << 32 | v.patch << 16;
|
||||
let version = (v.major << 48) | (v.minor << 32) | (v.patch << 16);
|
||||
res.set_version_info(VersionInfo::FILEVERSION, version);
|
||||
res.set_version_info(VersionInfo::PRODUCTVERSION, version);
|
||||
}
|
||||
@@ -636,6 +620,17 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
|
||||
res.set("ProductName", product_name);
|
||||
}
|
||||
|
||||
let company_name = config.bundle.publisher.unwrap_or_else(|| {
|
||||
config
|
||||
.identifier
|
||||
.split('.')
|
||||
.nth(1)
|
||||
.unwrap_or(&config.identifier)
|
||||
.to_string()
|
||||
});
|
||||
|
||||
res.set("CompanyName", &company_name);
|
||||
|
||||
let file_description = config
|
||||
.product_name
|
||||
.or_else(|| manifest.package.as_ref().map(|p| p.name.clone()))
|
||||
@@ -688,7 +683,7 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
|
||||
}
|
||||
}
|
||||
"msvc" => {
|
||||
if env::var("STATIC_VCRUNTIME").map_or(false, |v| v == "true") {
|
||||
if env::var_os("STATIC_VCRUNTIME").is_some_and(|v| v == "true") {
|
||||
static_vcruntime::build();
|
||||
}
|
||||
}
|
||||
@@ -703,23 +698,3 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct CargoMetadata {
|
||||
workspace_root: PathBuf,
|
||||
}
|
||||
|
||||
fn get_workspace_dir() -> Result<PathBuf> {
|
||||
let output = std::process::Command::new("cargo")
|
||||
.args(["metadata", "--no-deps", "--format-version", "1"])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"cargo metadata command exited with a non zero exit code: {}",
|
||||
String::from_utf8(output.stderr)?
|
||||
));
|
||||
}
|
||||
|
||||
Ok(serde_json::from_slice::<CargoMetadata>(&output.stdout)?.workspace_root)
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ struct AllowlistedDependency {
|
||||
name: String,
|
||||
alias: Option<String>,
|
||||
kind: DependencyKind,
|
||||
all_cli_managed_features: Option<Vec<&'static str>>,
|
||||
all_cli_managed_features: Vec<&'static str>,
|
||||
expected_features: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ pub fn check(config: &Config, manifest: &mut Manifest) -> Result<()> {
|
||||
name: "tauri-build".into(),
|
||||
alias: None,
|
||||
kind: DependencyKind::Build,
|
||||
all_cli_managed_features: Some(vec!["isolation"]),
|
||||
all_cli_managed_features: vec!["isolation"],
|
||||
expected_features: match config.app.security.pattern {
|
||||
PatternKind::Isolation { .. } => vec!["isolation".to_string()],
|
||||
_ => vec![],
|
||||
@@ -43,12 +43,10 @@ pub fn check(config: &Config, manifest: &mut Manifest) -> Result<()> {
|
||||
name: "tauri".into(),
|
||||
alias: None,
|
||||
kind: DependencyKind::Normal,
|
||||
all_cli_managed_features: Some(
|
||||
AppConfig::all_features()
|
||||
.into_iter()
|
||||
.filter(|f| f != &"tray-icon")
|
||||
.collect(),
|
||||
),
|
||||
all_cli_managed_features: AppConfig::all_features()
|
||||
.into_iter()
|
||||
.filter(|f| f != &"tray-icon")
|
||||
.collect(),
|
||||
expected_features: config
|
||||
.app
|
||||
.features()
|
||||
@@ -129,23 +127,13 @@ fn check_features(dependency: Dependency, metadata: &AllowlistedDependency) -> R
|
||||
Dependency::Inherited(dep) => dep.features,
|
||||
};
|
||||
|
||||
let diff = if let Some(all_cli_managed_features) = &metadata.all_cli_managed_features {
|
||||
features_diff(
|
||||
&features
|
||||
.into_iter()
|
||||
.filter(|f| all_cli_managed_features.contains(&f.as_str()))
|
||||
.collect::<Vec<String>>(),
|
||||
&metadata.expected_features,
|
||||
)
|
||||
} else {
|
||||
features_diff(
|
||||
&features
|
||||
.into_iter()
|
||||
.filter(|f| f.starts_with("allow-"))
|
||||
.collect::<Vec<String>>(),
|
||||
&metadata.expected_features,
|
||||
)
|
||||
};
|
||||
let diff = features_diff(
|
||||
&features
|
||||
.into_iter()
|
||||
.filter(|f| metadata.all_cli_managed_features.contains(&f.as_str()))
|
||||
.collect::<Vec<String>>(),
|
||||
&metadata.expected_features,
|
||||
);
|
||||
|
||||
let mut error_message = String::new();
|
||||
if !diff.remove.is_empty() {
|
||||
|
||||
@@ -2,26 +2,22 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{fs::write, path::PathBuf};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use semver::Version;
|
||||
use tauri_utils::{config::Config, write_if_changed};
|
||||
use tauri_utils::write_if_changed;
|
||||
|
||||
use crate::is_dev;
|
||||
|
||||
pub fn generate_gradle_files(project_dir: PathBuf, config: &Config) -> Result<()> {
|
||||
pub fn generate_gradle_files(project_dir: PathBuf) -> Result<()> {
|
||||
let gradle_settings_path = project_dir.join("tauri.settings.gradle");
|
||||
let app_build_gradle_path = project_dir.join("app").join("tauri.build.gradle.kts");
|
||||
let app_tauri_properties_path = project_dir.join("app").join("tauri.properties");
|
||||
|
||||
let mut gradle_settings =
|
||||
"// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.\n".to_string();
|
||||
let mut app_build_gradle = "// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
val implementation by configurations
|
||||
dependencies {"
|
||||
dependencies {
|
||||
implementation(\"androidx.lifecycle:lifecycle-process:2.10.0\")"
|
||||
.to_string();
|
||||
let mut app_tauri_properties = Vec::new();
|
||||
|
||||
for (env, value) in std::env::vars_os() {
|
||||
let env = env.to_string_lossy();
|
||||
@@ -54,32 +50,6 @@ dependencies {"
|
||||
|
||||
app_build_gradle.push_str("\n}");
|
||||
|
||||
if let Some(version) = config.version.as_ref() {
|
||||
app_tauri_properties.push(format!("tauri.android.versionName={version}"));
|
||||
if let Some(version_code) = config.bundle.android.version_code.as_ref() {
|
||||
app_tauri_properties.push(format!("tauri.android.versionCode={version_code}"));
|
||||
} else if let Ok(version) = Version::parse(version) {
|
||||
let mut version_code = version.major * 1000000 + version.minor * 1000 + version.patch;
|
||||
|
||||
if is_dev() {
|
||||
version_code = version_code.clamp(1, 2100000000);
|
||||
}
|
||||
|
||||
if version_code == 0 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"You must change the `version` in `tauri.conf.json`. The default value `0.0.0` is not allowed for Android package and must be at least `0.0.1`."
|
||||
));
|
||||
} else if version_code > 2100000000 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Invalid version code {}. Version code must be between 1 and 2100000000. You must change the `version` in `tauri.conf.json`.",
|
||||
version_code
|
||||
));
|
||||
}
|
||||
|
||||
app_tauri_properties.push(format!("tauri.android.versionCode={version_code}"));
|
||||
}
|
||||
}
|
||||
|
||||
// Overwrite only if changed to not trigger rebuilds
|
||||
write_if_changed(&gradle_settings_path, gradle_settings)
|
||||
.context("failed to write tauri.settings.gradle")?;
|
||||
@@ -87,28 +57,8 @@ dependencies {"
|
||||
write_if_changed(&app_build_gradle_path, app_build_gradle)
|
||||
.context("failed to write tauri.build.gradle.kts")?;
|
||||
|
||||
if !app_tauri_properties.is_empty() {
|
||||
let app_tauri_properties_content = format!(
|
||||
"// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.\n{}",
|
||||
app_tauri_properties.join("\n")
|
||||
);
|
||||
if std::fs::read_to_string(&app_tauri_properties_path)
|
||||
.map(|o| o != app_tauri_properties_content)
|
||||
.unwrap_or(true)
|
||||
{
|
||||
write(&app_tauri_properties_path, app_tauri_properties_content)
|
||||
.context("failed to write tauri.properties")?;
|
||||
}
|
||||
}
|
||||
|
||||
println!("cargo:rerun-if-changed={}", gradle_settings_path.display());
|
||||
println!("cargo:rerun-if-changed={}", app_build_gradle_path.display());
|
||||
if !app_tauri_properties.is_empty() {
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
app_tauri_properties_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,330 @@
|
||||
# Changelog
|
||||
|
||||
## \[2.8.1]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`6252432f0`](https://www.github.com/tauri-apps/tauri/commit/6252432f0757d896d7a61819bbff127efac5a156) ([#14945](https://www.github.com/tauri-apps/tauri/pull/14945) by [@veeceey](https://www.github.com/tauri-apps/tauri/../../veeceey)) Fix WIX installer registry search order so that the named `InstallDir` key takes priority over the NSIS default key, preventing install location from changing on updates.
|
||||
|
||||
### What's Changed
|
||||
|
||||
- [`7be58a1c6`](https://www.github.com/tauri-apps/tauri/commit/7be58a1c643a7ed6d0f484a7e1134022618eb2b2) ([#14894](https://www.github.com/tauri-apps/tauri/pull/14894) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Log patching bundle type information again
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.8.3`
|
||||
|
||||
## \[2.8.0]
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [`c769f211f`](https://www.github.com/tauri-apps/tauri/commit/c769f211fcaa543884c9d0f87ebd2ee106c01382) ([#14824](https://www.github.com/tauri-apps/tauri/pull/14824) by [@Kf637](https://www.github.com/tauri-apps/tauri/../../Kf637)) feat(nsis): add Norwegian language support for installer.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`7fca58230`](https://www.github.com/tauri-apps/tauri/commit/7fca58230f97c3e6834134419514a0c7dbbe784b) ([#14830](https://www.github.com/tauri-apps/tauri/pull/14830) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Updated `nsis_tauri_utils` to 0.5.3:
|
||||
|
||||
- Use an alternative method `CreateProcessWithTokenW` to run programs as user, this fixed a problem that the program launched with the previous method can't query its own handle
|
||||
|
||||
### What's Changed
|
||||
|
||||
- [`0575dd287`](https://www.github.com/tauri-apps/tauri/commit/0575dd287e021b61d2aedf64d62ae84a2c925fb4) ([#14521](https://www.github.com/tauri-apps/tauri/pull/14521) by [@kandrelczyk](https://www.github.com/tauri-apps/tauri/../../kandrelczyk)) Change the way bundle type information is added to binary files. Instead of looking up the value of a variable we simply look for the default value.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.8.2`
|
||||
- Upgraded to `tauri-macos-sign@2.3.3`
|
||||
|
||||
## \[2.7.5]
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [`4176f93ae`](https://www.github.com/tauri-apps/tauri/commit/4176f93ae43ef66714c4934feb3df19df3a3e28a) ([#14570](https://www.github.com/tauri-apps/tauri/pull/14570) by [@chfaft](https://www.github.com/tauri-apps/tauri/../../chfaft)) Consider extensions that are defined in the wxs template.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`018b4db22`](https://www.github.com/tauri-apps/tauri/commit/018b4db22e167fa67b37b0933e192a0f3556d3e5) ([#14625](https://www.github.com/tauri-apps/tauri/pull/14625) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Skip signing for NSIS uninstaller when using `--no-sign` flag
|
||||
- [`91becd9e4`](https://www.github.com/tauri-apps/tauri/commit/91becd9e4fa2db089ddc6b21dadc06133e939e08) ([#14627](https://www.github.com/tauri-apps/tauri/pull/14627) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix NSIS plugins not being signed due to wrong path handlings
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-macos-sign@2.3.2`
|
||||
|
||||
## \[2.7.4]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`1496145f8`](https://www.github.com/tauri-apps/tauri/commit/1496145f8222649efeff22b819a96208670bbea1) ([#14585](https://www.github.com/tauri-apps/tauri/pull/14585) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Fixed an issue that caused the AppImage bundler to fail with 404 errors for 32-bit builds.
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
- [`ce98d87ce`](https://www.github.com/tauri-apps/tauri/commit/ce98d87ce0aaa907285852eb80691197424e03c3) ([#14474](https://www.github.com/tauri-apps/tauri/pull/14474) by [@Tunglies](https://www.github.com/tauri-apps/tauri/../../Tunglies)) refactor: remove needless collect. No user facing changes.
|
||||
- [`ee3cc4a91`](https://www.github.com/tauri-apps/tauri/commit/ee3cc4a91bf1315ecaefe90f423ffd55ef6c40db) ([#14475](https://www.github.com/tauri-apps/tauri/pull/14475) by [@Tunglies](https://www.github.com/tauri-apps/tauri/../../Tunglies)) perf: remove needless clones in various files for improved performance. No user facing changes.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-macos-sign@2.3.1`
|
||||
- Upgraded to `tauri-utils@2.8.1`
|
||||
- [`b5ef603d8`](https://www.github.com/tauri-apps/tauri/commit/b5ef603d84bd8044625e50dcfdabb099b2e9fdd9) ([#14478](https://www.github.com/tauri-apps/tauri/pull/14478) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Updated NSIS from 3.8 to 3.11
|
||||
|
||||
## \[2.7.3]
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [`22edc65aa`](https://www.github.com/tauri-apps/tauri/commit/22edc65aad0b3e45515008e8e0866112da70c8a1) ([#14408](https://www.github.com/tauri-apps/tauri/pull/14408) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Set user-agent in bundler and cli http requests when fetching build tools.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`9a1922636`](https://www.github.com/tauri-apps/tauri/commit/9a192263693d71123a9953e2a6ee60fad07500b4) ([#14410](https://www.github.com/tauri-apps/tauri/pull/14410) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix uninstall fails if you close the app manually during the 'Click Ok to kill it' dialog
|
||||
|
||||
## \[2.7.2]
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [`7f710b8f3`](https://www.github.com/tauri-apps/tauri/commit/7f710b8f3b509ed327d76761926511cf56e66b2d) ([#14390](https://www.github.com/tauri-apps/tauri/pull/14390) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Inline linuxdeploy plugins which were previously downloaded from `https://raw.githubusercontent.com` which lately blocks many users with a 429 error.
|
||||
- [`fc017ee25`](https://www.github.com/tauri-apps/tauri/commit/fc017ee2577f48615367ea519386d3f37837e2c1) ([#14368](https://www.github.com/tauri-apps/tauri/pull/14368) by [@kandrelczyk](https://www.github.com/tauri-apps/tauri/../../kandrelczyk)) Mention symbol stripping on Linux in binary patch failed warning message
|
||||
|
||||
## \[2.7.1]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-macos-sign@2.3.0`
|
||||
|
||||
## \[2.7.0]
|
||||
|
||||
### New Features
|
||||
|
||||
- [`2a06d1006`](https://www.github.com/tauri-apps/tauri/commit/2a06d10066a806e392efe8bfb16d943ee0b0b61d) ([#14052](https://www.github.com/tauri-apps/tauri/pull/14052)) Add a `--no-sign` flag to the `tauri build` and `tauri bundle` commands to skip the code signing step, improving the developer experience for local testing and development without requiring code signing keys.
|
||||
- [`cc8c0b531`](https://www.github.com/tauri-apps/tauri/commit/cc8c0b53171173dbd1d01781a50de1a3ea159031) ([#14031](https://www.github.com/tauri-apps/tauri/pull/14031)) Support providing `plist::Value` as macOS entitlements.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [`b06b3bd09`](https://www.github.com/tauri-apps/tauri/commit/b06b3bd091b0fed26cdcfb23cacb0462a7a9cc2d) ([#14126](https://www.github.com/tauri-apps/tauri/pull/14126)) Improve error messages with more context.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`06d4a4ed6`](https://www.github.com/tauri-apps/tauri/commit/06d4a4ed6c146d6c7782016cf90037b56b944445) ([#14241](https://www.github.com/tauri-apps/tauri/pull/14241)) Set `APPIMAGE_EXTRACT_AND_RUN` on top of using the `--appimage-extra-and-run` cli arg for linuxdeploy.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.8.0`
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- [`ed7c9a410`](https://www.github.com/tauri-apps/tauri/commit/ed7c9a4100e08c002212265549d12130d021ad1e) ([#14108](https://www.github.com/tauri-apps/tauri/pull/14108)) Changed `MacOsSettings::info_plist_path` to `MacOsSettings::info_plist`.
|
||||
|
||||
## \[2.6.1]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`f3df96fb3`](https://www.github.com/tauri-apps/tauri/commit/f3df96fb38e2f27ce6bf232fe87f35bcfec50ce4) ([#14065](https://www.github.com/tauri-apps/tauri/pull/14065) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix binary patching updater type fails on 32 bit Windows builds
|
||||
|
||||
## \[2.6.0]
|
||||
|
||||
### New Features
|
||||
|
||||
- [`a9ec12843`](https://www.github.com/tauri-apps/tauri/commit/a9ec12843aa7d0eb774bd3a53e2e63da12cfa77b) ([#13521](https://www.github.com/tauri-apps/tauri/pull/13521) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Added a `--skip-stapling` option to make `tauri build|bundle` *not* wait for notarization to finish on macOS.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [`8b465a12b`](https://www.github.com/tauri-apps/tauri/commit/8b465a12ba73e94d7a3995defd9cc362d15eeebe) ([#13913](https://www.github.com/tauri-apps/tauri/pull/13913) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) The bundler now pulls the latest AppImage linuxdeploy plugin instead of using the built-in one. This should remove the libfuse requirement.
|
||||
- [`4475e93e1`](https://www.github.com/tauri-apps/tauri/commit/4475e93e136e9e2bd5f3c7817fa2040924f630f6) ([#13824](https://www.github.com/tauri-apps/tauri/pull/13824) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) The bundler and cli will now read TLS Certificates installed on the system when downloading tools and checking versions.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`a8f1569b0`](https://www.github.com/tauri-apps/tauri/commit/a8f1569b04edf7b54a19e19ad37b421b0808f512) ([#13921](https://www.github.com/tauri-apps/tauri/pull/13921) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) The bundler will no longer try to sign non-binary and already signed binary files on Windows
|
||||
- [`bc6b125b2`](https://www.github.com/tauri-apps/tauri/commit/bc6b125b24589ffc412a4f17d899a387a0fc0bb2) ([#13909](https://www.github.com/tauri-apps/tauri/pull/13909) by [@Andrew15-5](https://www.github.com/tauri-apps/tauri/../../Andrew15-5)) The bundler now falls back to `1` for the release in case an empty string was provided instead of using `-.` in the file name.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.7.0`
|
||||
- Upgraded to `tauri-macos-sign@2.2.0`
|
||||
|
||||
## \[2.5.2]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`af95fb601`](https://www.github.com/tauri-apps/tauri/commit/af95fb6014ea54a2636bfd299095608f6cd93221) ([#13870](https://www.github.com/tauri-apps/tauri/pull/13870) by [@kittuov](https://www.github.com/tauri-apps/tauri/../../kittuov)) The bundler now signs the main binary after patching it for every package type on windows
|
||||
|
||||
## \[2.5.1]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`f94af9035`](https://www.github.com/tauri-apps/tauri/commit/f94af90359ec8b01138ae542391caa704ec18ca8) ([#13786](https://www.github.com/tauri-apps/tauri/pull/13786) by [@catalinsh](https://www.github.com/tauri-apps/tauri/../../catalinsh)) Fix NSIS per-machine installer not requesting elevation when run by non-admin users.
|
||||
- [`f2dbe7309`](https://www.github.com/tauri-apps/tauri/commit/f2dbe730979d570be3ee3ecac9621204c4ceb788) ([#13772](https://www.github.com/tauri-apps/tauri/pull/13772) by [@catalinsh](https://www.github.com/tauri-apps/tauri/../../catalinsh)) Fix incorrect expected file path for `nsis_tauri_utils.dll` resulting in tauri-cli re-downloading the file on every build.
|
||||
- [`7a6fd5b75`](https://www.github.com/tauri-apps/tauri/commit/7a6fd5b75d61071e2771f6277c0376ec206d302a) ([#13863](https://www.github.com/tauri-apps/tauri/pull/13863) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) The AppImage bundler now pulls the AppRun binaries from our GitHub mirror, fixing 404 errors.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.6.0`
|
||||
|
||||
## \[2.5.0]
|
||||
|
||||
### New Features
|
||||
|
||||
- [`414619c36`](https://www.github.com/tauri-apps/tauri/commit/414619c36e94e21939534dd72c0438b93da75546) ([#13536](https://www.github.com/tauri-apps/tauri/pull/13536) by [@Tunglies](https://www.github.com/tauri-apps/tauri/../../Tunglies)) Added support for the `bundleName` property in the macOS bundler configuration. This allows specifying the `CFBundleName` value for generated macOS bundles.
|
||||
- [`7322f0579`](https://www.github.com/tauri-apps/tauri/commit/7322f057923aaec88960ad5556776774b745762f) ([#13502](https://www.github.com/tauri-apps/tauri/pull/13502) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Allow using `CheckIfAppIsRunning` macro inside NSIS hooks, for example `!insertmacro CheckIfAppIsRunning "another-executable.exe" "Another Executable"`.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`479cee3d3`](https://www.github.com/tauri-apps/tauri/commit/479cee3d3680f9020005bdfb380d3a9482e286a1) ([#13260](https://www.github.com/tauri-apps/tauri/pull/13260) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) The bundler now sets the `ARCH` env var to the current build target to prevent potential issues with `appimagetool`'s auto-detection.
|
||||
- [`e045fe32c`](https://www.github.com/tauri-apps/tauri/commit/e045fe32c9b0bed954916dc42528e28ee19f75b8) ([#13334](https://www.github.com/tauri-apps/tauri/pull/13334) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Fix custom Windows sign command failing to sign app uninstaller if it references relative paths.
|
||||
- [`bd8a7cf39`](https://www.github.com/tauri-apps/tauri/commit/bd8a7cf39df316bf27c73a303d5e650301af0104) ([#13581](https://www.github.com/tauri-apps/tauri/pull/13581) by [@martpie](https://www.github.com/tauri-apps/tauri/../../martpie)) Fixes app icon not being displayed on Gnome dock and grid view when using Wayland.
|
||||
- [`b52da29d5`](https://www.github.com/tauri-apps/tauri/commit/b52da29d5dbdb675ddba438a335e6a59f620e536) ([#13429](https://www.github.com/tauri-apps/tauri/pull/13429) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix `mainBinaryName` doesn't work when there's `.` in it
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.5.0`
|
||||
|
||||
## \[2.4.0]
|
||||
|
||||
### New Features
|
||||
|
||||
- [`b0babb6df`](https://www.github.com/tauri-apps/tauri/commit/b0babb6df12dafe45c21a2c9c424fd86ffd75ca7) ([#12938](https://www.github.com/tauri-apps/tauri/pull/12938)) Added hebrew translation for the custom Tauri messages in the NSIS bundle.
|
||||
- [`0aa48fb9e`](https://www.github.com/tauri-apps/tauri/commit/0aa48fb9e4b9d7b5bf3522000a76ebc1836394ed) ([#13030](https://www.github.com/tauri-apps/tauri/pull/13030)) Added `bundleVersion` to iOS and macOS configuration to support specifying a `CFBundleVersion`.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [`8d994f60f`](https://www.github.com/tauri-apps/tauri/commit/8d994f60fe05ec0f45cbe926506bbe10b0d36e3c) ([#11676](https://www.github.com/tauri-apps/tauri/pull/11676)) Sign NSIS and WiX DLLs when bundling
|
||||
- [`8d994f60f`](https://www.github.com/tauri-apps/tauri/commit/8d994f60fe05ec0f45cbe926506bbe10b0d36e3c) ([#11676](https://www.github.com/tauri-apps/tauri/pull/11676)) Sign DLLs from resources.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`9ea76503d`](https://www.github.com/tauri-apps/tauri/commit/9ea76503dcf8da11fab65550f4ab8d3565a424ef) ([#13186](https://www.github.com/tauri-apps/tauri/pull/13186)) Fix NSIS bundler can't include resources and sidecars with `$` in the path
|
||||
- [`2dccfab53`](https://www.github.com/tauri-apps/tauri/commit/2dccfab5321fef55d45f3a4c674b6151b1c4424a) ([#13236](https://www.github.com/tauri-apps/tauri/pull/13236)) Fix `fileAssociations` missing `LSHandlerRank` on macOS.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.4.0`
|
||||
|
||||
## \[2.3.1]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`2138bbc21`](https://www.github.com/tauri-apps/tauri/commit/2138bbc21294785df5f4144670104387289f79c1) ([#13087](https://www.github.com/tauri-apps/tauri/pull/13087) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix NSIS installer displaying in wrong language if `SpanishInternational` is included
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.3.1`
|
||||
|
||||
## \[2.3.0]
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [`f981a5ee8`](https://www.github.com/tauri-apps/tauri/commit/f981a5ee8b292b9ea09329f60cecc7f688dda734) ([#12602](https://www.github.com/tauri-apps/tauri/pull/12602) by [@kxxt](https://www.github.com/tauri-apps/tauri/../../kxxt)) Add basic support for linux riscv64 platform.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`3626b7a92`](https://www.github.com/tauri-apps/tauri/commit/3626b7a92be2890a82e8d5bd00d13887e199ea4a) ([#12759](https://www.github.com/tauri-apps/tauri/pull/12759) by [@ninjadev64](https://www.github.com/tauri-apps/tauri/../../ninjadev64)) Fix resources being bundled to the wrong path during RPM bundling when resources are specified as a map.
|
||||
- [`2b960dfd9`](https://www.github.com/tauri-apps/tauri/commit/2b960dfd9fdc995bd6474958c05783ff53b64b7e) ([#12643](https://www.github.com/tauri-apps/tauri/pull/12643) by [@animeshchaudhri](https://www.github.com/tauri-apps/tauri/../../animeshchaudhri)) Remove the autostart plugin registry entry when the app is uninstalled (NSIS only).
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.3.0`
|
||||
|
||||
## \[2.2.4]
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [`5eba0785c`](https://www.github.com/tauri-apps/tauri/commit/5eba0785c461a0d0bec47653eaf6ccdf5f05d347) ([#12605](https://www.github.com/tauri-apps/tauri/pull/12605) by [@niusia-ua](https://www.github.com/tauri-apps/tauri/../../niusia-ua)) Added Ukrainian translation for the custom tauri messages in the nsis bundle
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.2.0`
|
||||
- Upgraded to `tauri-macos-sign@2.1.0`
|
||||
|
||||
## \[2.2.3]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`de8600b4d`](https://www.github.com/tauri-apps/tauri/commit/de8600b4d9a04e809e078c8aea61825d1328201f) ([#12471](https://www.github.com/tauri-apps/tauri/pull/12471) by [@anatawa12](https://www.github.com/tauri-apps/tauri/../../anatawa12)) Bumped `nsis-tauri-utils` to `0.4.2` which fixes the following bugs:
|
||||
|
||||
- Fixed launch on start checkbox in nsis installer does not work well with applications that require elevated permissions
|
||||
- Fixed nsis installer may fail to install if launched by updater plugin
|
||||
- [`fbe7c9ead`](https://www.github.com/tauri-apps/tauri/commit/fbe7c9ead76e71ca258c6f48bbb62185fcc37b1c) ([#12466](https://www.github.com/tauri-apps/tauri/pull/12466) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Fixed an issue that caused the compiled AppImage to miss webkitgtk's internal `libwebkit2gtkinjectedbundle.so` file.
|
||||
- [`f5a59b93b`](https://www.github.com/tauri-apps/tauri/commit/f5a59b93bfefb43ff131a7870b3c5d5e48c1ca1e) ([#12136](https://www.github.com/tauri-apps/tauri/pull/12136) by [@unknovvn](https://www.github.com/tauri-apps/tauri/../../unknovvn)) The NSIS bundler will now replace non-numeric build metadata with `0` instead of returning an error.
|
||||
- [`9dac2863a`](https://www.github.com/tauri-apps/tauri/commit/9dac2863afa70fb0bcddf859b284afba917f28ae) ([#12323](https://www.github.com/tauri-apps/tauri/pull/12323) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Skip signing the .dmg if self signing via `"signingIdentity": "-"` is used.
|
||||
- [`b8eb28877`](https://www.github.com/tauri-apps/tauri/commit/b8eb28877fe822dbe17999fc8af98ed7d0983679) ([#12427](https://www.github.com/tauri-apps/tauri/pull/12427) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Clean up `Software\${MANUFACTURER}\${PRODUCTNAME}` registry key in the NSIS uninstaller if "Delete application data" option is checked when uninstalling.
|
||||
|
||||
## \[2.2.2]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`72748cc45`](https://www.github.com/tauri-apps/tauri/commit/72748cc45cf670dd03c86c8deceb5942598f5ad9) ([#12365](https://www.github.com/tauri-apps/tauri/pull/12365) by [@don41382](https://www.github.com/tauri-apps/tauri/../../don41382)) Fixed an issue that caused the `.msi` installer not to lookup the `INSTALLDIR` set in the `nsis` installer.
|
||||
- [`cf771bf69`](https://www.github.com/tauri-apps/tauri/commit/cf771bf69aa26b62d11a54a69131c631505d8c55) ([#12402](https://www.github.com/tauri-apps/tauri/pull/12402) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Fixed an issue that caused the .msi installer to not contain root resources when there were .dll files present in the target directory.
|
||||
- [`07ccdc499`](https://www.github.com/tauri-apps/tauri/commit/07ccdc499c3240e7240be3abf95ef2d7d00b2dc7) ([#12324](https://www.github.com/tauri-apps/tauri/pull/12324) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Fixed an issue leading to NSIS based installers to not contain the `WebView2Loader.dll` file when targetting `windows-gnu`.
|
||||
|
||||
## \[2.2.1]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`cd1d026f9`](https://www.github.com/tauri-apps/tauri/commit/cd1d026f9799c26b04acb64f49e7ee0a8b193049) ([#11961](https://www.github.com/tauri-apps/tauri/pull/11961) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix tauri fails to build if the project path contains glob characters
|
||||
|
||||
## \[2.2.0]
|
||||
|
||||
### New Features
|
||||
|
||||
- [`cccb308c7`](https://www.github.com/tauri-apps/tauri/commit/cccb308c7b559b0838138d6cea280665f060c925) ([#11562](https://www.github.com/tauri-apps/tauri/pull/11562) by [@jLynx](https://www.github.com/tauri-apps/tauri/../../jLynx)) Generate signature for `.deb` packages when `createUpdaterArtifacts` option is enabled.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [`93a3a043d`](https://www.github.com/tauri-apps/tauri/commit/93a3a043d39cc96515d51d98beeb14261d3a246b) ([#11727](https://www.github.com/tauri-apps/tauri/pull/11727) by [@Kiyozz](https://www.github.com/tauri-apps/tauri/../../Kiyozz)) Add support for `Portuguese` language for NSIS windows installer.
|
||||
- [`53f808674`](https://www.github.com/tauri-apps/tauri/commit/53f808674b2c0012bc44a41ced90e742afbb41e8) ([#11799](https://www.github.com/tauri-apps/tauri/pull/11799) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) The bundler now reads the `TAURI_BUNDLER_DMG_IGNORE_CI` env var to decide whether to check for `CI: true` when building DMG files.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.1.1`
|
||||
|
||||
## \[2.1.0]
|
||||
|
||||
### New Features
|
||||
|
||||
- [`1b6b2cfaa`](https://www.github.com/tauri-apps/tauri/commit/1b6b2cfaa14ab1d418c676cedbf942a812377a30) ([#11521](https://www.github.com/tauri-apps/tauri/pull/11521) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Process `bundle > windows > wix > fragmentPaths` with Handlebars to interpolate expressions within it.
|
||||
- [`6dea12a06`](https://www.github.com/tauri-apps/tauri/commit/6dea12a0677a905cb1f14969fe05c53e7cd717c6) ([#11402](https://www.github.com/tauri-apps/tauri/pull/11402) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `bundle > linux > deb > recommends` and `bundle > linux > rpm > recommends` fields to declare a strong, but not absolute, dependency for your `.deb` and `.rpm` packages.
|
||||
- [`058c0db72`](https://www.github.com/tauri-apps/tauri/commit/058c0db72f43fbe1574d0db654560e693755cd7e) ([#11584](https://www.github.com/tauri-apps/tauri/pull/11584) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `bundle > linux > rpm > compression` config option to control RPM bundle compression type and level.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.1.0`
|
||||
|
||||
## \[2.0.4]
|
||||
|
||||
### New Features
|
||||
|
||||
- [`c8f55b615`](https://www.github.com/tauri-apps/tauri/commit/c8f55b615d2d98ade5c0f1896139dc283382a176) ([#11388](https://www.github.com/tauri-apps/tauri/pull/11388) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `bundler > windows > wix > version` to manually specify a wix-compatible version.
|
||||
|
||||
## \[2.0.3]
|
||||
|
||||
### New Features
|
||||
|
||||
- [`eda5713ea`](https://www.github.com/tauri-apps/tauri/commit/eda5713eab78d28182071ea25ceca5f1994f37ea) ([#11242](https://www.github.com/tauri-apps/tauri/pull/11242) by [@alex-sandri](https://www.github.com/tauri-apps/tauri/../../alex-sandri)) Add `Italian` to supported NSIS installer languages
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [`504bb8ec8`](https://www.github.com/tauri-apps/tauri/commit/504bb8ec8cb294c5067357e18328580dd2b950c9) ([#11287](https://www.github.com/tauri-apps/tauri/pull/11287) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Pull upstream changes for the DMG creation script.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`069c05e44`](https://www.github.com/tauri-apps/tauri/commit/069c05e44fd6f30083fdc00dd6c0001278898592) ([#11315](https://www.github.com/tauri-apps/tauri/pull/11315) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Fix CLI crashing and failing to find a `.ico` file when `bundle > icon` option is using globs and doesn't have a string that ends with `.ico`.
|
||||
|
||||
## \[2.0.2]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`858b3516a`](https://www.github.com/tauri-apps/tauri/commit/858b3516a008ae5e6f2af489805896e2c142be10) ([#11217](https://www.github.com/tauri-apps/tauri/pull/11217) by [@kittuov](https://www.github.com/tauri-apps/tauri/../../kittuov)) On Windows, fixed command arguments for `bundle -> windows -> msi -> elevatedUpdateTask`. to work with spaces in `productName`
|
||||
- [`a49a19ffa`](https://www.github.com/tauri-apps/tauri/commit/a49a19ffa304f031fb1a04d31a567cc7f42a380a) ([#11218](https://www.github.com/tauri-apps/tauri/pull/11218)) Fix bundling `appimage`, `deb` and `rpm` bundles failing to open when using `mainBinaryName` with spaces.
|
||||
|
||||
## \[2.0.1]
|
||||
|
||||
### What's Changed
|
||||
|
||||
- [`0ab2b3306`](https://www.github.com/tauri-apps/tauri/commit/0ab2b330644b6419f6cee1d5377bfb5cdda2ccf9) ([#11205](https://www.github.com/tauri-apps/tauri/pull/11205) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.0.1`
|
||||
- Upgraded to `tauri-macos-sign@2.0.1`
|
||||
|
||||
## \[2.0.0]
|
||||
|
||||
### What's Changed
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tauri-bundler"
|
||||
version = "2.0.0"
|
||||
version = "2.8.1"
|
||||
authors = [
|
||||
"George Burton <burtonageo@gmail.com>",
|
||||
"Tauri Programme within The Commons Conservancy",
|
||||
@@ -11,68 +11,72 @@ keywords = ["bundle", "cargo", "tauri"]
|
||||
repository = "https://github.com/tauri-apps/tauri"
|
||||
description = "Wrap rust executables in OS-specific app bundles for Tauri"
|
||||
edition = "2021"
|
||||
rust-version = "1.78"
|
||||
rust-version = "1.77.2"
|
||||
exclude = ["CHANGELOG.md", "/target", "rustfmt.toml"]
|
||||
|
||||
[dependencies]
|
||||
tauri-utils = { version = "2.0.0", path = "../tauri-utils", features = [
|
||||
tauri-utils = { version = "2.8.3", path = "../tauri-utils", features = [
|
||||
"resources",
|
||||
] }
|
||||
image = "0.25.0"
|
||||
flate2 = "1.0"
|
||||
anyhow = "1.0"
|
||||
thiserror = "1.0"
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
strsim = "0.11.0"
|
||||
tar = "0.4.40"
|
||||
image = "0.25"
|
||||
flate2 = "1"
|
||||
thiserror = "2"
|
||||
anyhow = "1"
|
||||
serde_json = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
strsim = "0.11"
|
||||
tar = "0.4"
|
||||
walkdir = "2"
|
||||
handlebars = "6"
|
||||
tempfile = "3.10.1"
|
||||
tempfile = "3"
|
||||
log = { version = "0.4.21", features = ["kv"] }
|
||||
dirs = "5"
|
||||
dirs = "6"
|
||||
os_pipe = "1"
|
||||
ureq = { version = "2.9.6", default-features = false, features = [
|
||||
"socks-proxy",
|
||||
] }
|
||||
ureq = { version = "3", default-features = false, features = ["socks-proxy"] }
|
||||
native-tls = { version = "0.2", optional = true }
|
||||
hex = "0.4"
|
||||
semver = "1"
|
||||
sha1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
zip = { version = "2.0", default-features = false, features = ["deflate"] }
|
||||
zip = { version = "4", default-features = false, features = ["deflate"] }
|
||||
dunce = "1"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["v4", "v5"] }
|
||||
regex = "1"
|
||||
goblin = "0.9"
|
||||
plist = "1"
|
||||
|
||||
|
||||
[target."cfg(target_os = \"windows\")".dependencies]
|
||||
bitness = "0.4"
|
||||
windows-registry = "0.2.0"
|
||||
windows-registry = "0.5"
|
||||
glob = "0.3"
|
||||
|
||||
[target."cfg(target_os = \"windows\")".dependencies.windows-sys]
|
||||
version = "0.59"
|
||||
version = "0.60"
|
||||
features = ["Win32_System_SystemInformation", "Win32_System_Diagnostics_Debug"]
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
icns = { package = "tauri-icns", version = "0.1" }
|
||||
time = { version = "0.3", features = ["formatting"] }
|
||||
plist = "1"
|
||||
tauri-macos-sign = { version = "2.0.0", path = "../tauri-macos-sign" }
|
||||
tauri-macos-sign = { version = "2.3.3", path = "../tauri-macos-sign" }
|
||||
|
||||
[target."cfg(target_os = \"linux\")".dependencies]
|
||||
heck = "0.5"
|
||||
ar = "0.9.0"
|
||||
md5 = "0.7.0"
|
||||
rpm = "0.15.0"
|
||||
ar = "0.9"
|
||||
md5 = "0.8"
|
||||
rpm = { version = "0.16", features = ["bzip2-compression"] }
|
||||
|
||||
[target."cfg(unix)".dependencies]
|
||||
which = "8"
|
||||
|
||||
[lib]
|
||||
name = "tauri_bundler"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
default = ["rustls"]
|
||||
default = ["rustls", "platform-certs"]
|
||||
native-tls = ["ureq/native-tls"]
|
||||
native-tls-vendored = ["native-tls", "native-tls/vendored"]
|
||||
rustls = ["ureq/tls"]
|
||||
rustls = ["ureq/rustls"]
|
||||
platform-certs = ["ureq/platform-verifier"]
|
||||
|
||||
@@ -4,32 +4,94 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
mod category;
|
||||
mod common;
|
||||
mod kmp;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
mod path_utils;
|
||||
mod platform;
|
||||
mod settings;
|
||||
mod updater_bundle;
|
||||
mod windows;
|
||||
|
||||
use tauri_utils::display_path;
|
||||
use tauri_utils::{display_path, platform::Target as TargetPlatform};
|
||||
|
||||
const BUNDLE_VAR_TOKEN: &[u8] = b"__TAURI_BUNDLE_TYPE_VAR_UNK";
|
||||
/// Patch a binary with bundle type information
|
||||
fn patch_binary(binary: &PathBuf, package_type: &PackageType) -> crate::Result<()> {
|
||||
#[cfg(target_os = "linux")]
|
||||
let bundle_type = match package_type {
|
||||
crate::PackageType::Deb => b"__TAURI_BUNDLE_TYPE_VAR_DEB",
|
||||
crate::PackageType::Rpm => b"__TAURI_BUNDLE_TYPE_VAR_RPM",
|
||||
crate::PackageType::AppImage => b"__TAURI_BUNDLE_TYPE_VAR_APP",
|
||||
// NSIS installers can be built in linux using cargo-xwin
|
||||
crate::PackageType::Nsis => b"__TAURI_BUNDLE_TYPE_VAR_NSS",
|
||||
_ => {
|
||||
return Err(crate::Error::InvalidPackageType(
|
||||
package_type.short_name().to_owned(),
|
||||
"Linux".to_owned(),
|
||||
))
|
||||
}
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
let bundle_type = match package_type {
|
||||
crate::PackageType::Nsis => b"__TAURI_BUNDLE_TYPE_VAR_NSS",
|
||||
crate::PackageType::WindowsMsi => b"__TAURI_BUNDLE_TYPE_VAR_MSI",
|
||||
_ => {
|
||||
return Err(crate::Error::InvalidPackageType(
|
||||
package_type.short_name().to_owned(),
|
||||
"Windows".to_owned(),
|
||||
))
|
||||
}
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
let bundle_type = match package_type {
|
||||
// NSIS installers can be built in macOS using cargo-xwin
|
||||
crate::PackageType::Nsis => b"__TAURI_BUNDLE_TYPE_VAR_NSS",
|
||||
crate::PackageType::MacOsBundle | crate::PackageType::Dmg => {
|
||||
// skip patching for macOS-native bundles
|
||||
return Ok(());
|
||||
}
|
||||
_ => {
|
||||
return Err(crate::Error::InvalidPackageType(
|
||||
package_type.short_name().to_owned(),
|
||||
"macOS".to_owned(),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
log::info!(
|
||||
"Patching {} with bundle type information: {}",
|
||||
display_path(binary),
|
||||
package_type.short_name()
|
||||
);
|
||||
|
||||
let mut file_data = std::fs::read(binary).expect("Could not read binary file.");
|
||||
let bundle_var_index =
|
||||
kmp::index_of(BUNDLE_VAR_TOKEN, &file_data).ok_or(crate::Error::MissingBundleTypeVar)?;
|
||||
file_data[bundle_var_index..bundle_var_index + BUNDLE_VAR_TOKEN.len()]
|
||||
.copy_from_slice(bundle_type);
|
||||
|
||||
std::fs::write(binary, &file_data).map_err(|e| crate::Error::BinaryWriteError(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub use self::{
|
||||
category::AppCategory,
|
||||
settings::{
|
||||
AppImageSettings, BundleBinary, BundleSettings, CustomSignCommandSettings, DebianSettings,
|
||||
DmgSettings, MacOsSettings, PackageSettings, PackageType, Position, RpmSettings, Settings,
|
||||
SettingsBuilder, Size, UpdaterSettings,
|
||||
DmgSettings, Entitlements, IosSettings, MacOsSettings, PackageSettings, PackageType, PlistKind,
|
||||
Position, RpmSettings, Settings, SettingsBuilder, Size, UpdaterSettings,
|
||||
},
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
use anyhow::Context;
|
||||
pub use settings::{NsisSettings, WindowsSettings, WixLanguage, WixLanguageConfig, WixSettings};
|
||||
|
||||
use std::{fmt::Write, path::PathBuf};
|
||||
use std::{
|
||||
fmt::Write,
|
||||
io::{Seek, SeekFrom},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
/// Generated bundle metadata.
|
||||
#[derive(Debug)]
|
||||
@@ -50,50 +112,32 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<Bundle>> {
|
||||
|
||||
package_types.sort_by_key(|a| a.priority());
|
||||
|
||||
let target_os = settings
|
||||
.target()
|
||||
.split('-')
|
||||
.nth(2)
|
||||
.unwrap_or(std::env::consts::OS)
|
||||
.replace("darwin", "macos");
|
||||
let target_os = settings.target_platform();
|
||||
|
||||
if target_os != std::env::consts::OS {
|
||||
if *target_os != TargetPlatform::current() {
|
||||
log::warn!("Cross-platform compilation is experimental and does not support all features. Please use a matching host system for full compatibility.");
|
||||
}
|
||||
|
||||
// Sign windows binaries before the bundling step in case neither wix and nsis bundles are enabled
|
||||
if target_os == "windows" {
|
||||
if settings.can_sign() {
|
||||
for bin in settings.binaries() {
|
||||
let bin_path = settings.binary_path(bin);
|
||||
windows::sign::try_sign(&bin_path, settings)?;
|
||||
}
|
||||
sign_binaries_if_needed(settings, target_os)?;
|
||||
|
||||
// Sign the sidecar binaries
|
||||
for bin in settings.external_binaries() {
|
||||
let path = bin?;
|
||||
let skip =
|
||||
std::env::var("TAURI_SKIP_SIDECAR_SIGNATURE_CHECK").map_or(false, |v| v == "true");
|
||||
if skip {
|
||||
continue;
|
||||
}
|
||||
let main_binary = settings
|
||||
.binaries()
|
||||
.iter()
|
||||
.find(|b| b.main())
|
||||
.expect("Main binary missing in settings");
|
||||
let main_binary_path = settings.binary_path(main_binary);
|
||||
|
||||
#[cfg(windows)]
|
||||
if windows::sign::verify(&path)? {
|
||||
log::info!(
|
||||
"sidecar at \"{}\" already signed. Skipping...",
|
||||
path.display()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
windows::sign::try_sign(&path, settings)?;
|
||||
}
|
||||
} else {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
log::warn!("Signing, by default, is only supported on Windows hosts, but you can specify a custom signing command in `bundler > windows > sign_command`, for now, skipping signing the installer...");
|
||||
}
|
||||
}
|
||||
// We make a copy of the unsigned main_binary so that we can restore it after each package_type step.
|
||||
// This allows us to patch the binary correctly and avoids two issues:
|
||||
// - modifying a signed binary without updating its PE checksum can break signature verification
|
||||
// - codesigning tools should handle calculating+updating this, we just need to ensure
|
||||
// (re)signing is performed after every `patch_binary()` operation
|
||||
// - signing an already-signed binary can result in multiple signatures, causing verification errors
|
||||
// TODO: change this to work on a copy while preserving the main binary unchanged
|
||||
let mut main_binary_copy = tempfile::tempfile()?;
|
||||
let mut main_binary_orignal = std::fs::File::open(&main_binary_path)?;
|
||||
std::io::copy(&mut main_binary_orignal, &mut main_binary_copy)?;
|
||||
|
||||
let mut bundles = Vec::<Bundle>::new();
|
||||
for package_type in &package_types {
|
||||
@@ -102,6 +146,15 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<Bundle>> {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(e) = patch_binary(&main_binary_path, package_type) {
|
||||
log::warn!("Failed to add bundler type to the binary: {e}. Updater plugin may not be able to update this package. This shouldn't normally happen, please report it to https://github.com/tauri-apps/tauri/issues");
|
||||
}
|
||||
|
||||
// sign main binary for every package type after patch
|
||||
if matches!(target_os, TargetPlatform::Windows) && settings.windows().can_sign() {
|
||||
windows::sign::try_sign(&main_binary_path, settings)?;
|
||||
}
|
||||
|
||||
let bundle_paths = match package_type {
|
||||
#[cfg(target_os = "macos")]
|
||||
PackageType::MacOsBundle => macos::app::bundle_project(settings)?,
|
||||
@@ -122,6 +175,7 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<Bundle>> {
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
PackageType::WindowsMsi => windows::msi::bundle_project(settings, false)?,
|
||||
// don't restrict to windows as NSIS installers can be built in linux+macOS using cargo-xwin
|
||||
PackageType::Nsis => windows::nsis::bundle_project(settings, false)?,
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -140,6 +194,14 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<Bundle>> {
|
||||
package_type: package_type.to_owned(),
|
||||
bundle_paths,
|
||||
});
|
||||
|
||||
// Restore unsigned and unpatched binary
|
||||
let mut modified_main_binary = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(&main_binary_path)?;
|
||||
main_binary_copy.seek(SeekFrom::Start(0))?;
|
||||
std::io::copy(&mut main_binary_copy, &mut modified_main_binary)?;
|
||||
}
|
||||
|
||||
if let Some(updater) = settings.updater() {
|
||||
@@ -151,6 +213,7 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<Bundle>> {
|
||||
| PackageType::MacOsBundle
|
||||
| PackageType::Nsis
|
||||
| PackageType::WindowsMsi
|
||||
| PackageType::Deb
|
||||
)
|
||||
} else {
|
||||
matches!(package_type, PackageType::MacOsBundle)
|
||||
@@ -166,7 +229,7 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<Bundle>> {
|
||||
// Self contained updater, no need to zip
|
||||
matches!(
|
||||
package_type,
|
||||
PackageType::AppImage | PackageType::Nsis | PackageType::WindowsMsi
|
||||
PackageType::AppImage | PackageType::Nsis | PackageType::WindowsMsi | PackageType::Deb
|
||||
)
|
||||
})
|
||||
{
|
||||
@@ -188,31 +251,30 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<Bundle>> {
|
||||
.map(|b| b.bundle_paths)
|
||||
{
|
||||
for app_bundle_path in &app_bundle_paths {
|
||||
use crate::error::ErrorExt;
|
||||
|
||||
log::info!(action = "Cleaning"; "{}", app_bundle_path.display());
|
||||
match app_bundle_path.is_dir() {
|
||||
true => std::fs::remove_dir_all(app_bundle_path),
|
||||
false => std::fs::remove_file(app_bundle_path),
|
||||
}
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to clean the app bundle at {}",
|
||||
app_bundle_path.display()
|
||||
)
|
||||
})?
|
||||
.fs_context(
|
||||
"failed to clean the app bundle",
|
||||
app_bundle_path.to_path_buf(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if bundles.is_empty() {
|
||||
return Err(anyhow::anyhow!("No bundles were built").into());
|
||||
return Ok(bundles);
|
||||
}
|
||||
|
||||
let bundles_wo_updater = bundles
|
||||
let finished_bundles = bundles
|
||||
.iter()
|
||||
.filter(|b| b.package_type != PackageType::Updater)
|
||||
.collect::<Vec<_>>();
|
||||
let finished_bundles = bundles_wo_updater.len();
|
||||
.count();
|
||||
let pluralised = if finished_bundles == 1 {
|
||||
"bundle"
|
||||
} else {
|
||||
@@ -237,6 +299,51 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<Bundle>> {
|
||||
Ok(bundles)
|
||||
}
|
||||
|
||||
fn sign_binaries_if_needed(settings: &Settings, target_os: &TargetPlatform) -> crate::Result<()> {
|
||||
if matches!(target_os, TargetPlatform::Windows) {
|
||||
if settings.windows().can_sign() {
|
||||
if settings.no_sign() {
|
||||
log::warn!("Skipping binary signing due to --no-sign flag.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for bin in settings.binaries() {
|
||||
if bin.main() {
|
||||
// we will sign the main binary after patching per "package type"
|
||||
continue;
|
||||
}
|
||||
let bin_path = settings.binary_path(bin);
|
||||
windows::sign::try_sign(&bin_path, settings)?;
|
||||
}
|
||||
|
||||
// Sign the sidecar binaries
|
||||
for bin in settings.external_binaries() {
|
||||
let path = bin?;
|
||||
let skip = std::env::var("TAURI_SKIP_SIDECAR_SIGNATURE_CHECK").is_ok_and(|v| v == "true");
|
||||
if skip {
|
||||
continue;
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
if windows::sign::verify(&path)? {
|
||||
log::info!(
|
||||
"sidecar at \"{}\" already signed. Skipping...",
|
||||
path.display()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
windows::sign::try_sign(&path, settings)?;
|
||||
}
|
||||
} else {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
log::warn!("Signing, by default, is only supported on Windows hosts, but you can specify a custom signing command in `bundler > windows > sign_command`, for now, skipping signing the installer...");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check to see if there are icons in the settings struct
|
||||
pub fn check_icons(settings: &Settings) -> crate::Result<bool> {
|
||||
// make a peekable iterator of the icon_files
|
||||
|
||||
@@ -249,7 +249,7 @@ struct AppCategoryVisitor {
|
||||
did_you_mean: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl<'d> serde::de::Visitor<'d> for AppCategoryVisitor {
|
||||
impl serde::de::Visitor<'_> for AppCategoryVisitor {
|
||||
type Value = AppCategory;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
|
||||
59
crates/tauri-bundler/src/bundle/kmp/mod.rs
Normal file
59
crates/tauri-bundler/src/bundle/kmp/mod.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright 2016-2019 Cargo-Bundle developers <https://github.com/burtonageo/cargo-bundle>
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Knuth–Morris–Pratt algorithm
|
||||
// based on https://github.com/howeih/rust_kmp
|
||||
pub fn index_of(pattern: &[u8], target: &[u8]) -> Option<usize> {
|
||||
let failure_function = find_failure_function(pattern);
|
||||
|
||||
let mut t_i: usize = 0;
|
||||
let mut p_i: usize = 0;
|
||||
let target_len = target.len();
|
||||
let mut result_idx = None;
|
||||
let pattern_len = pattern.len();
|
||||
|
||||
while (t_i < target_len) && (p_i < pattern_len) {
|
||||
if target[t_i] == pattern[p_i] {
|
||||
if result_idx.is_none() {
|
||||
result_idx.replace(t_i);
|
||||
}
|
||||
t_i += 1;
|
||||
p_i += 1;
|
||||
if p_i >= pattern_len {
|
||||
return result_idx;
|
||||
}
|
||||
} else {
|
||||
if p_i == 0 {
|
||||
p_i = 0;
|
||||
t_i += 1;
|
||||
} else {
|
||||
p_i = failure_function[p_i - 1];
|
||||
}
|
||||
result_idx = None;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn find_failure_function(pattern: &[u8]) -> Vec<usize> {
|
||||
let mut i = 1;
|
||||
let mut j = 0;
|
||||
let pattern_length = pattern.len();
|
||||
let end_i = pattern_length - 1;
|
||||
let mut failure_function = vec![0usize; pattern_length];
|
||||
while i <= end_i {
|
||||
if pattern[i] == pattern[j] {
|
||||
failure_function[i] = j + 1;
|
||||
i += 1;
|
||||
j += 1;
|
||||
} else if j == 0 {
|
||||
failure_function[i] = 0;
|
||||
i += 1;
|
||||
} else {
|
||||
j = failure_function[j - 1];
|
||||
}
|
||||
}
|
||||
failure_function
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
export ARCH={{arch}}
|
||||
APPIMAGE_BUNDLE_XDG_OPEN=${APPIMAGE_BUNDLE_XDG_OPEN-0}
|
||||
APPIMAGE_BUNDLE_XDG_MIME=${APPIMAGE_BUNDLE_XDG_MIME-0}
|
||||
APPIMAGE_BUNDLE_GSTREAMER=${APPIMAGE_BUNDLE_GSTREAMER-0}
|
||||
TAURI_TRAY_LIBRARY_PATH=${TAURI_TRAY_LIBRARY_PATH-0}
|
||||
|
||||
if [ "$ARCH" == "i686" ]; then
|
||||
linuxdeploy_arch="i386"
|
||||
else
|
||||
linuxdeploy_arch="$ARCH"
|
||||
fi
|
||||
|
||||
mkdir -p "{{product_name}}.AppDir"
|
||||
cp -r ../appimage_deb/data/usr "{{product_name}}.AppDir"
|
||||
|
||||
cd "{{product_name}}.AppDir"
|
||||
mkdir -p "usr/bin"
|
||||
mkdir -p "usr/lib"
|
||||
|
||||
if [[ "$APPIMAGE_BUNDLE_XDG_OPEN" != "0" ]] && [[ -f "/usr/bin/xdg-open" ]]; then
|
||||
echo "Copying /usr/bin/xdg-open"
|
||||
cp /usr/bin/xdg-open usr/bin
|
||||
fi
|
||||
|
||||
if [[ "$APPIMAGE_BUNDLE_XDG_MIME" != "0" ]] && [[ -f "/usr/bin/xdg-mime" ]]; then
|
||||
echo "Copying /usr/bin/xdg-mime"
|
||||
cp /usr/bin/xdg-mime usr/bin
|
||||
fi
|
||||
|
||||
if [[ "$TAURI_TRAY_LIBRARY_PATH" != "0" ]]; then
|
||||
echo "Copying appindicator library ${TAURI_TRAY_LIBRARY_PATH}"
|
||||
cp ${TAURI_TRAY_LIBRARY_PATH} usr/lib
|
||||
# It looks like we're practicing good hygiene by adding the ABI version.
|
||||
# But for compatibility we'll symlink this file to what we did before.
|
||||
# Specifically this prevents breaking libappindicator-sys v0.7.1 and v0.7.2.
|
||||
if [[ "$TAURI_TRAY_LIBRARY_PATH" == *.so.1 ]]; then
|
||||
readonly soname=$(basename "$TAURI_TRAY_LIBRARY_PATH")
|
||||
readonly old_name=$(basename "$TAURI_TRAY_LIBRARY_PATH" .1)
|
||||
echo "Adding compatibility symlink ${old_name} -> ${soname}"
|
||||
ln -s ${soname} usr/lib/${old_name}
|
||||
fi
|
||||
fi
|
||||
|
||||
# Copy WebKit files. Follow symlinks in case `/usr/lib64` is a symlink to `/usr/lib`
|
||||
find -L /usr/lib* -name WebKitNetworkProcess -exec mkdir -p "$(dirname '{}')" \; -exec cp --parents '{}' "." \; || true
|
||||
find -L /usr/lib* -name WebKitWebProcess -exec mkdir -p "$(dirname '{}')" \; -exec cp --parents '{}' "." \; || true
|
||||
find -L /usr/lib* -name libwebkit2gtkinjectedbundle.so -exec mkdir -p "$(dirname '{}')" \; -exec cp --parents '{}' "." \; || true
|
||||
|
||||
( cd "{{tauri_tools_path}}" && ( wget -q -4 -N https://github.com/AppImage/AppImageKit/releases/download/continuous/AppRun-${ARCH} || wget -q -4 -N https://github.com/AppImage/AppImageKit/releases/download/12/AppRun-${ARCH} ) )
|
||||
chmod +x "{{tauri_tools_path}}/AppRun-${ARCH}"
|
||||
|
||||
# We need AppRun to be installed as {{product_name}}.AppDir/AppRun.
|
||||
# Otherwise the linuxdeploy scripts will default to symlinking our main bin instead and will crash on trying to launch.
|
||||
cp "{{tauri_tools_path}}/AppRun-${ARCH}" AppRun
|
||||
|
||||
cp "{{icon_path}}" .DirIcon
|
||||
ln -sf "{{icon_path}}" "{{product_name}}.png"
|
||||
|
||||
ln -sf "usr/share/applications/{{product_name}}.desktop" "{{product_name}}.desktop"
|
||||
|
||||
cd ..
|
||||
|
||||
if [[ "$APPIMAGE_BUNDLE_GSTREAMER" != "0" ]]; then
|
||||
gst_plugin="--plugin gstreamer"
|
||||
wget -q -4 -N "https://raw.githubusercontent.com/tauri-apps/linuxdeploy-plugin-gstreamer/master/linuxdeploy-plugin-gstreamer.sh"
|
||||
chmod +x linuxdeploy-plugin-gstreamer.sh
|
||||
else
|
||||
gst_plugin=""
|
||||
fi
|
||||
|
||||
( cd "{{tauri_tools_path}}" && wget -q -4 -N https://raw.githubusercontent.com/tauri-apps/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh )
|
||||
( cd "{{tauri_tools_path}}" && wget -q -4 -N https://github.com/tauri-apps/binary-releases/releases/download/linuxdeploy/linuxdeploy-${linuxdeploy_arch}.AppImage )
|
||||
|
||||
chmod +x "{{tauri_tools_path}}/linuxdeploy-plugin-gtk.sh"
|
||||
chmod +x "{{tauri_tools_path}}/linuxdeploy-${linuxdeploy_arch}.AppImage"
|
||||
|
||||
dd if=/dev/zero bs=1 count=3 seek=8 conv=notrunc of="{{tauri_tools_path}}/linuxdeploy-${linuxdeploy_arch}.AppImage"
|
||||
|
||||
OUTPUT="{{appimage_filename}}" "{{tauri_tools_path}}/linuxdeploy-${linuxdeploy_arch}.AppImage" --appimage-extract-and-run --appdir "{{product_name}}.AppDir" --plugin gtk ${gst_plugin} --output appimage
|
||||
@@ -0,0 +1,165 @@
|
||||
#! /bin/bash
|
||||
|
||||
# abort on all errors
|
||||
set -e
|
||||
|
||||
if [ "$DEBUG" != "" ]; then
|
||||
set -x
|
||||
fi
|
||||
|
||||
script=$(readlink -f "$0")
|
||||
|
||||
show_usage() {
|
||||
echo "Usage: $script --appdir <path to AppDir>"
|
||||
echo
|
||||
echo "Bundles GStreamer plugins into an AppDir"
|
||||
echo
|
||||
echo "Required variables:"
|
||||
echo " LINUXDEPLOY=\".../linuxdeploy\" path to linuxdeploy (e.g., AppImage); set automatically when plugin is run directly by linuxdeploy"
|
||||
echo
|
||||
echo "Optional variables:"
|
||||
echo " GSTREAMER_INCLUDE_BAD_PLUGINS=\"1\" (default: disabled; set to empty string or unset to disable)"
|
||||
echo " GSTREAMER_PLUGINS_DIR=\"...\" (directory containing GStreamer plugins; default: guessed based on main distro architecture)"
|
||||
echo " GSTREAMER_HELPERS_DIR=\"...\" (directory containing GStreamer helper tools like gst-plugin-scanner; default: guessed based on main distro architecture)"
|
||||
echo " GSTREAMER_VERSION=\"1.0\" (default: 1.0)"
|
||||
}
|
||||
|
||||
while [ "$1" != "" ]; do
|
||||
case "$1" in
|
||||
--plugin-api-version)
|
||||
echo "0"
|
||||
exit 0
|
||||
;;
|
||||
--appdir)
|
||||
APPDIR="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
show_usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Invalid argument: $1"
|
||||
echo
|
||||
show_usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$APPDIR" == "" ]; then
|
||||
show_usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! which patchelf &>/dev/null && ! type patchelf &>/dev/null; then
|
||||
echo "Error: patchelf not found"
|
||||
echo
|
||||
show_usage
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ "$LINUXDEPLOY" == "" ]]; then
|
||||
echo "Error: \$LINUXDEPLOY not set"
|
||||
echo
|
||||
show_usage
|
||||
exit 3
|
||||
fi
|
||||
|
||||
mkdir -p "$APPDIR"
|
||||
|
||||
export GSTREAMER_VERSION="${GSTREAMER_VERSION:-1.0}"
|
||||
|
||||
plugins_target_dir="$APPDIR"/usr/lib/gstreamer-"$GSTREAMER_VERSION"
|
||||
helpers_target_dir="$APPDIR"/usr/lib/gstreamer"$GSTREAMER_VERSION"/gstreamer-"$GSTREAMER_VERSION"
|
||||
|
||||
if [ "$GSTREAMER_PLUGINS_DIR" != "" ]; then
|
||||
plugins_dir="${GSTREAMER_PLUGINS_DIR}"
|
||||
elif [ -d /usr/lib/"$(uname -m)"-linux-gnu/gstreamer-"$GSTREAMER_VERSION" ]; then
|
||||
plugins_dir=/usr/lib/$(uname -m)-linux-gnu/gstreamer-"$GSTREAMER_VERSION"
|
||||
else
|
||||
plugins_dir=/usr/lib/gstreamer-"$GSTREAMER_VERSION"
|
||||
fi
|
||||
|
||||
if [ "$GSTREAMER_HELPERS_DIR" != "" ]; then
|
||||
helpers_dir="${GSTREAMER_HELPERS_DIR}"
|
||||
else
|
||||
helpers_dir=/usr/lib/$(uname -m)-linux-gnu/gstreamer"$GSTREAMER_VERSION"/gstreamer-"$GSTREAMER_VERSION"
|
||||
fi
|
||||
|
||||
if [ ! -d "$plugins_dir" ]; then
|
||||
echo "Error: could not find plugins directory: $plugins_dir"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$plugins_target_dir"
|
||||
|
||||
echo "Copying plugins into $plugins_target_dir"
|
||||
for i in "$plugins_dir"/*; do
|
||||
[ -d "$i" ] && continue
|
||||
[ ! -f "$i" ] && echo "File does not exist: $i" && continue
|
||||
|
||||
echo "Copying plugin: $i"
|
||||
cp "$i" "$plugins_target_dir"
|
||||
done
|
||||
|
||||
"$LINUXDEPLOY" --appdir "$APPDIR"
|
||||
|
||||
for i in "$plugins_target_dir"/*; do
|
||||
[ -d "$i" ] && continue
|
||||
[ ! -f "$i" ] && echo "File does not exist: $i" && continue
|
||||
(file "$i" | grep -v ELF --silent) && echo "Ignoring non ELF file: $i" && continue
|
||||
|
||||
echo "Manually setting rpath for $i"
|
||||
patchelf --set-rpath '$ORIGIN/..:$ORIGIN' "$i"
|
||||
done
|
||||
|
||||
mkdir -p "$helpers_target_dir"
|
||||
|
||||
echo "Copying helpers in $helpers_target_dir"
|
||||
for i in "$helpers_dir"/*; do
|
||||
[ -d "$i" ] && continue
|
||||
[ ! -f "$i" ] && echo "File does not exist: $i" && continue
|
||||
|
||||
echo "Copying helper: $i"
|
||||
cp "$i" "$helpers_target_dir"
|
||||
done
|
||||
|
||||
for i in "$helpers_target_dir"/*; do
|
||||
[ -d "$i" ] && continue
|
||||
[ ! -f "$i" ] && echo "File does not exist: $i" && continue
|
||||
(file "$i" | grep -v ELF --silent) && echo "Ignoring non ELF file: $i" && continue
|
||||
|
||||
echo "Manually setting rpath for $i"
|
||||
patchelf --set-rpath '$ORIGIN/../..' "$i"
|
||||
done
|
||||
|
||||
echo "Installing AppRun hook"
|
||||
mkdir -p "$APPDIR"/apprun-hooks
|
||||
|
||||
if [ "$GSTREAMER_VERSION" == "1.0" ]; then
|
||||
cat > "$APPDIR"/apprun-hooks/linuxdeploy-plugin-gstreamer.sh <<\EOF
|
||||
#! /bin/bash
|
||||
|
||||
export GST_REGISTRY_REUSE_PLUGIN_SCANNER="no"
|
||||
export GST_PLUGIN_SYSTEM_PATH_1_0="${APPDIR}/usr/lib/gstreamer-1.0"
|
||||
export GST_PLUGIN_PATH_1_0="${APPDIR}/usr/lib/gstreamer-1.0"
|
||||
|
||||
export GST_PLUGIN_SCANNER_1_0="${APPDIR}/usr/lib/gstreamer1.0/gstreamer-1.0/gst-plugin-scanner"
|
||||
export GST_PTP_HELPER_1_0="${APPDIR}/usr/lib/gstreamer1.0/gstreamer-1.0/gst-ptp-helper"
|
||||
EOF
|
||||
elif [ "$GSTREAMER_VERSION" == "0.10" ]; then
|
||||
cat > "$APPDIR"/apprun-hooks/linuxdeploy-plugin-gstreamer.sh <<\EOF
|
||||
#! /bin/bash
|
||||
|
||||
export GST_REGISTRY_REUSE_PLUGIN_SCANNER="no"
|
||||
export GST_PLUGIN_SYSTEM_PATH_0_10="${APPDIR}/usr/lib/gstreamer-1.0"
|
||||
|
||||
export GST_PLUGIN_SCANNER_0_10="${APPDIR}/usr/lib/gstreamer1.0/gstreamer-1.0/gst-plugin-scanner"
|
||||
export GST_PTP_HELPER_0_10="${APPDIR}/usr/lib/gstreamer1.0/gstreamer-1.0/gst-ptp-helper"
|
||||
EOF
|
||||
else
|
||||
echo "Warning: unknown GStreamer version: $GSTREAMER_VERSION, cannot install AppRun hook"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
#! /usr/bin/env bash
|
||||
|
||||
# GTK3 environment variables: https://docs.gtk.org/gtk3/running.html
|
||||
# GTK4 environment variables: https://docs.gtk.org/gtk4/running.html
|
||||
|
||||
# abort on all errors
|
||||
set -e
|
||||
|
||||
if [ "$DEBUG" != "" ]; then
|
||||
set -x
|
||||
verbose="--verbose"
|
||||
fi
|
||||
|
||||
script=$(readlink -f "$0")
|
||||
|
||||
show_usage() {
|
||||
echo "Usage: $script --appdir <path to AppDir>"
|
||||
echo
|
||||
echo "Bundles resources for applications that use GTK into an AppDir"
|
||||
echo
|
||||
echo "Required variables:"
|
||||
echo " LINUXDEPLOY=\".../linuxdeploy\" path to linuxdeploy (e.g., AppImage); set automatically when plugin is run directly by linuxdeploy"
|
||||
#echo
|
||||
#echo "Optional variables:"
|
||||
#echo " DEPLOY_GTK_VERSION (major version of GTK to deploy, e.g. '2', '3' or '4'; auto-detect by default)"
|
||||
}
|
||||
|
||||
variable_is_true() {
|
||||
local var="$1"
|
||||
|
||||
if [ -n "$var" ] && { [ "$var" == "true" ] || [ "$var" -gt 0 ]; } 2> /dev/null; then
|
||||
return 0 # true
|
||||
else
|
||||
return 1 # false
|
||||
fi
|
||||
}
|
||||
|
||||
get_pkgconf_variable() {
|
||||
local variable="$1"
|
||||
local library="$2"
|
||||
local default_path="$3"
|
||||
|
||||
path="$("$PKG_CONFIG" --variable="$variable" "$library")"
|
||||
if [ -n "$path" ]; then
|
||||
echo "$path"
|
||||
elif [ -n "$default_path" ]; then
|
||||
echo "$default_path"
|
||||
else
|
||||
echo "$0: there is no '$variable' variable for '$library' library." > /dev/stderr
|
||||
echo "Please check the '$library.pc' file is present in \$PKG_CONFIG_PATH (you may need to install the appropriate -dev/-devel package)." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
copy_tree() {
|
||||
local src=("${@:1:$#-1}")
|
||||
local dst="${*:$#}"
|
||||
|
||||
for elem in "${src[@]}"; do
|
||||
mkdir -p "${dst::-1}$elem"
|
||||
cp "$elem" --archive --parents --target-directory="$dst" $verbose
|
||||
done
|
||||
}
|
||||
|
||||
search_tool() {
|
||||
local tool="$1"
|
||||
local directory="$2"
|
||||
|
||||
if command -v "$tool"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
PATH_ARRAY=(
|
||||
"/usr/lib/$(uname -m)-linux-gnu/$directory/$tool"
|
||||
"/usr/lib/$directory/$tool"
|
||||
"/usr/bin/$tool"
|
||||
"/usr/bin/$tool-64"
|
||||
"/usr/bin/$tool-32"
|
||||
)
|
||||
|
||||
for path in "${PATH_ARRAY[@]}"; do
|
||||
if [ -x "$path" ]; then
|
||||
echo "$path"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
#DEPLOY_GTK_VERSION="${DEPLOY_GTK_VERSION:-0}" # When not set by user, this variable use the integer '0' as a sentinel value
|
||||
DEPLOY_GTK_VERSION=3 # Force GTK3 for tauri apps
|
||||
APPDIR=
|
||||
|
||||
while [ "$1" != "" ]; do
|
||||
case "$1" in
|
||||
--plugin-api-version)
|
||||
echo "0"
|
||||
exit 0
|
||||
;;
|
||||
--appdir)
|
||||
APPDIR="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
show_usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Invalid argument: $1"
|
||||
echo
|
||||
show_usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$APPDIR" == "" ]; then
|
||||
show_usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$APPDIR"
|
||||
# make lib64 writable again.
|
||||
chmod +w "$APPDIR"/usr/lib64 || true
|
||||
|
||||
if command -v pkgconf > /dev/null; then
|
||||
PKG_CONFIG="pkgconf"
|
||||
elif command -v pkg-config > /dev/null; then
|
||||
PKG_CONFIG="pkg-config"
|
||||
else
|
||||
echo "$0: pkg-config/pkgconf not found in PATH, aborting"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v find &>/dev/null && ! type find &>/dev/null; then
|
||||
echo -e "$0: find not found.\nInstall findutils then re-run the plugin."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$LINUXDEPLOY" ]; then
|
||||
echo -e "$0: LINUXDEPLOY environment variable is not set.\nDownload a suitable linuxdeploy AppImage, set the environment variable and re-run the plugin."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
gtk_versions=0 # Count major versions of GTK when auto-detect GTK version
|
||||
if [ "$DEPLOY_GTK_VERSION" -eq 0 ]; then
|
||||
echo "Determining which GTK version to deploy"
|
||||
while IFS= read -r -d '' file; do
|
||||
if [ "$DEPLOY_GTK_VERSION" -ne 2 ] && ldd "$file" | grep -q "libgtk-x11-2.0.so"; then
|
||||
DEPLOY_GTK_VERSION=2
|
||||
gtk_versions="$((gtk_versions+1))"
|
||||
fi
|
||||
if [ "$DEPLOY_GTK_VERSION" -ne 3 ] && ldd "$file" | grep -q "libgtk-3.so"; then
|
||||
DEPLOY_GTK_VERSION=3
|
||||
gtk_versions="$((gtk_versions+1))"
|
||||
fi
|
||||
if [ "$DEPLOY_GTK_VERSION" -ne 4 ] && ldd "$file" | grep -q "libgtk-4.so"; then
|
||||
DEPLOY_GTK_VERSION=4
|
||||
gtk_versions="$((gtk_versions+1))"
|
||||
fi
|
||||
done < <(find "$APPDIR/usr/bin" -executable -type f -print0)
|
||||
fi
|
||||
|
||||
if [ "$gtk_versions" -gt 1 ]; then
|
||||
echo "$0: can not deploy multiple GTK versions at the same time."
|
||||
echo "Please set DEPLOY_GTK_VERSION to {2, 3, 4}."
|
||||
exit 1
|
||||
elif [ "$DEPLOY_GTK_VERSION" -eq 0 ]; then
|
||||
echo "$0: failed to auto-detect GTK version."
|
||||
echo "Please set DEPLOY_GTK_VERSION to {2, 3, 4}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installing AppRun hook"
|
||||
HOOKSDIR="$APPDIR/apprun-hooks"
|
||||
HOOKFILE="$HOOKSDIR/linuxdeploy-plugin-gtk.sh"
|
||||
mkdir -p "$HOOKSDIR"
|
||||
cat > "$HOOKFILE" <<\EOF
|
||||
#! /usr/bin/env bash
|
||||
|
||||
gsettings get org.gnome.desktop.interface gtk-theme 2> /dev/null | grep -qi "dark" && GTK_THEME_VARIANT="dark" || GTK_THEME_VARIANT="light"
|
||||
APPIMAGE_GTK_THEME="${APPIMAGE_GTK_THEME:-"Adwaita:$GTK_THEME_VARIANT"}" # Allow user to override theme (discouraged)
|
||||
|
||||
export APPDIR="${APPDIR:-"$(dirname "$(realpath "$0")")"}" # Workaround to run extracted AppImage
|
||||
export GTK_DATA_PREFIX="$APPDIR"
|
||||
export GTK_THEME="$APPIMAGE_GTK_THEME" # Custom themes are broken
|
||||
export GDK_BACKEND=x11 # Crash with Wayland backend on Wayland - We tested it without it and ended up with this: https://github.com/tauri-apps/tauri/issues/8541
|
||||
export XDG_DATA_DIRS="$APPDIR/usr/share:/usr/share:$XDG_DATA_DIRS" # g_get_system_data_dirs() from GLib
|
||||
EOF
|
||||
|
||||
echo "Installing GLib schemas"
|
||||
# Note: schemasdir is undefined on Ubuntu 16.04
|
||||
glib_schemasdir="$(get_pkgconf_variable "schemasdir" "gio-2.0" "/usr/share/glib-2.0/schemas")"
|
||||
copy_tree "$glib_schemasdir" "$APPDIR/"
|
||||
glib-compile-schemas "$APPDIR/$glib_schemasdir"
|
||||
cat >> "$HOOKFILE" <<EOF
|
||||
export GSETTINGS_SCHEMA_DIR="\$APPDIR/$glib_schemasdir"
|
||||
EOF
|
||||
|
||||
case "$DEPLOY_GTK_VERSION" in
|
||||
2)
|
||||
# https://github.com/linuxdeploy/linuxdeploy-plugin-gtk/pull/20#issuecomment-826354261
|
||||
echo "WARNING: Gtk+2 applications are not fully supported by this plugin"
|
||||
;;
|
||||
3)
|
||||
echo "Installing GTK 3.0 modules"
|
||||
gtk3_exec_prefix="$(get_pkgconf_variable "exec_prefix" "gtk+-3.0")"
|
||||
gtk3_libdir="$(get_pkgconf_variable "libdir" "gtk+-3.0")/gtk-3.0"
|
||||
#gtk3_path="$gtk3_libdir/modules" export GTK_PATH="\$APPDIR/$gtk3_path"
|
||||
gtk3_immodulesdir="$gtk3_libdir/$(get_pkgconf_variable "gtk_binary_version" "gtk+-3.0")/immodules"
|
||||
gtk3_printbackendsdir="$gtk3_libdir/$(get_pkgconf_variable "gtk_binary_version" "gtk+-3.0")/printbackends"
|
||||
gtk3_immodules_cache_file="$(dirname "$gtk3_immodulesdir")/immodules.cache"
|
||||
gtk3_immodules_query="$(search_tool "gtk-query-immodules-3.0" "libgtk-3-0")"
|
||||
copy_tree "$gtk3_libdir" "$APPDIR/"
|
||||
cat >> "$HOOKFILE" <<EOF
|
||||
export GTK_EXE_PREFIX="\$APPDIR/$gtk3_exec_prefix"
|
||||
export GTK_PATH="\$APPDIR/$gtk3_libdir:/usr/lib64/gtk-3.0:/usr/lib/x86_64-linux-gnu/gtk-3.0"
|
||||
export GTK_IM_MODULE_FILE="\$APPDIR/$gtk3_immodules_cache_file"
|
||||
|
||||
EOF
|
||||
if [ -x "$gtk3_immodules_query" ]; then
|
||||
echo "Updating immodules cache in $APPDIR/$gtk3_immodules_cache_file"
|
||||
"$gtk3_immodules_query" > "$APPDIR/$gtk3_immodules_cache_file"
|
||||
else
|
||||
echo "WARNING: gtk-query-immodules-3.0 not found"
|
||||
fi
|
||||
if [ ! -f "$APPDIR/$gtk3_immodules_cache_file" ]; then
|
||||
echo "WARNING: immodules.cache file is missing"
|
||||
fi
|
||||
sed -i "s|$gtk3_libdir/3.0.0/immodules/||g" "$APPDIR/$gtk3_immodules_cache_file"
|
||||
;;
|
||||
4)
|
||||
echo "Installing GTK 4.0 modules"
|
||||
gtk4_exec_prefix="$(get_pkgconf_variable "exec_prefix" "gtk4" "/usr")"
|
||||
gtk4_libdir="$(get_pkgconf_variable "libdir" "gtk4")/gtk-4.0"
|
||||
gtk4_path="$gtk4_libdir/modules"
|
||||
copy_tree "$gtk4_libdir" "$APPDIR/"
|
||||
cat >> "$HOOKFILE" <<EOF
|
||||
export GTK_EXE_PREFIX="\$APPDIR/$gtk4_exec_prefix"
|
||||
export GTK_PATH="\$APPDIR/$gtk4_path"
|
||||
EOF
|
||||
;;
|
||||
*)
|
||||
echo "$0: '$DEPLOY_GTK_VERSION' is not a valid GTK major version."
|
||||
echo "Please set DEPLOY_GTK_VERSION to {2, 3, 4}."
|
||||
exit 1
|
||||
esac
|
||||
|
||||
echo "Installing GDK PixBufs"
|
||||
gdk_libdir="$(get_pkgconf_variable "libdir" "gdk-pixbuf-2.0")"
|
||||
gdk_pixbuf_binarydir="$(get_pkgconf_variable "gdk_pixbuf_binarydir" "gdk-pixbuf-2.0")"
|
||||
gdk_pixbuf_cache_file="$(get_pkgconf_variable "gdk_pixbuf_cache_file" "gdk-pixbuf-2.0")"
|
||||
gdk_pixbuf_moduledir="$(get_pkgconf_variable "gdk_pixbuf_moduledir" "gdk-pixbuf-2.0")"
|
||||
# Note: gdk_pixbuf_query_loaders variable is not defined on some systems
|
||||
gdk_pixbuf_query="$(search_tool "gdk-pixbuf-query-loaders" "gdk-pixbuf-2.0")"
|
||||
copy_tree "$gdk_pixbuf_binarydir" "$APPDIR/"
|
||||
cat >> "$HOOKFILE" <<EOF
|
||||
export GDK_PIXBUF_MODULE_FILE="\$APPDIR/$gdk_pixbuf_cache_file"
|
||||
EOF
|
||||
if [ -x "$gdk_pixbuf_query" ]; then
|
||||
echo "Updating pixbuf cache in $APPDIR/$gdk_pixbuf_cache_file"
|
||||
"$gdk_pixbuf_query" > "$APPDIR/$gdk_pixbuf_cache_file"
|
||||
else
|
||||
echo "WARNING: gdk-pixbuf-query-loaders not found"
|
||||
fi
|
||||
if [ ! -f "$APPDIR/$gdk_pixbuf_cache_file" ]; then
|
||||
echo "WARNING: loaders.cache file is missing"
|
||||
fi
|
||||
sed -i "s|$gdk_pixbuf_moduledir/||g" "$APPDIR/$gdk_pixbuf_cache_file"
|
||||
|
||||
echo "Copying more libraries"
|
||||
gobject_libdir="$(get_pkgconf_variable "libdir" "gobject-2.0")"
|
||||
gio_libdir="$(get_pkgconf_variable "libdir" "gio-2.0")"
|
||||
librsvg_libdir="$(get_pkgconf_variable "libdir" "librsvg-2.0")"
|
||||
pango_libdir="$(get_pkgconf_variable "libdir" "pango")"
|
||||
pangocairo_libdir="$(get_pkgconf_variable "libdir" "pangocairo")"
|
||||
pangoft2_libdir="$(get_pkgconf_variable "libdir" "pangoft2")"
|
||||
FIND_ARRAY=(
|
||||
"$gdk_libdir" "libgdk_pixbuf-*.so*"
|
||||
"$gobject_libdir" "libgobject-*.so*"
|
||||
"$gio_libdir" "libgio-*.so*"
|
||||
"$librsvg_libdir" "librsvg-*.so*"
|
||||
"$pango_libdir" "libpango-*.so*"
|
||||
"$pangocairo_libdir" "libpangocairo-*.so*"
|
||||
"$pangoft2_libdir" "libpangoft2-*.so*"
|
||||
)
|
||||
LIBRARIES=()
|
||||
for (( i=0; i<${#FIND_ARRAY[@]}; i+=2 )); do
|
||||
directory=${FIND_ARRAY[i]}
|
||||
library=${FIND_ARRAY[i+1]}
|
||||
while IFS= read -r -d '' file; do
|
||||
LIBRARIES+=( "--library=$file" )
|
||||
done < <(find "$directory" \( -type l -o -type f \) -name "$library" -print0)
|
||||
done
|
||||
|
||||
env LINUXDEPLOY_PLUGIN_MODE=1 "$LINUXDEPLOY" --appdir="$APPDIR" "${LIBRARIES[@]}"
|
||||
|
||||
# Create symbolic links as a workaround
|
||||
# Details: https://github.com/linuxdeploy/linuxdeploy-plugin-gtk/issues/24#issuecomment-1030026529
|
||||
echo "Manually setting rpath for GTK modules"
|
||||
PATCH_ARRAY=(
|
||||
"$gtk3_immodulesdir"
|
||||
"$gtk3_printbackendsdir"
|
||||
"$gdk_pixbuf_moduledir"
|
||||
)
|
||||
for directory in "${PATCH_ARRAY[@]}"; do
|
||||
while IFS= read -r -d '' file; do
|
||||
ln $verbose -s "${file/\/usr\/lib\//}" "$APPDIR/usr/lib"
|
||||
done < <(find "$directory" -name '*.so' -print0)
|
||||
done
|
||||
|
||||
# set write permission on lib64 again to make it deletable.
|
||||
chmod +w "$APPDIR"/usr/lib64 || true
|
||||
|
||||
# We have to copy the files first to not get permission errors when we assign gio_extras_dir
|
||||
find /usr/lib* -name libgiognutls.so -exec mkdir -p "$APPDIR"/"$(dirname '{}')" \; -exec cp --parents '{}' "$APPDIR/" \; || true
|
||||
# related files that we seemingly don't need:
|
||||
# libgiolibproxy.so - libgiognomeproxy.so - glib-pacrunner
|
||||
|
||||
gio_extras_dir=$(find "$APPDIR"/usr/lib* -name libgiognutls.so -exec dirname '{}' \; 2>/dev/null)
|
||||
cat >> "$HOOKFILE" <<EOF
|
||||
export GIO_EXTRA_MODULES="\$APPDIR/${gio_extras_dir#"$APPDIR"/}"
|
||||
EOF
|
||||
|
||||
#binary patch absolute paths in libwebkit files
|
||||
find "$APPDIR"/usr/lib* -name 'libwebkit*' -exec sed -i -e "s|/usr|././|g" '{}' \;
|
||||
|
||||
282
crates/tauri-bundler/src/bundle/linux/appimage/linuxdeploy.rs
Normal file
282
crates/tauri-bundler/src/bundle/linux/appimage/linuxdeploy.rs
Normal file
@@ -0,0 +1,282 @@
|
||||
// Copyright 2016-2019 Cargo-Bundle developers <https://github.com/burtonageo/cargo-bundle>
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use super::{super::debian, write_and_make_executable};
|
||||
use crate::{
|
||||
bundle::settings::Arch,
|
||||
error::{Context, ErrorExt},
|
||||
utils::{fs_utils, http_utils::download, CommandExt},
|
||||
Settings,
|
||||
};
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
/// Bundles the project.
|
||||
/// Returns a vector of PathBuf that shows where the AppImage was created.
|
||||
pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
// generate the deb binary name
|
||||
let appimage_arch: &str = match settings.binary_arch() {
|
||||
Arch::X86_64 => "amd64",
|
||||
Arch::X86 => "i386",
|
||||
Arch::AArch64 => "aarch64",
|
||||
Arch::Armhf => "armhf",
|
||||
target => {
|
||||
return Err(crate::Error::ArchError(format!(
|
||||
"Unsupported architecture: {target:?}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let tools_arch = if settings.binary_arch() == Arch::Armhf {
|
||||
"armhf"
|
||||
} else {
|
||||
settings.target().split('-').next().unwrap()
|
||||
};
|
||||
|
||||
let output_path = settings.project_out_directory().join("bundle/appimage");
|
||||
if output_path.exists() {
|
||||
fs::remove_dir_all(&output_path)?;
|
||||
}
|
||||
|
||||
let tools_path = settings
|
||||
.local_tools_directory()
|
||||
.map(|d| d.join(".tauri"))
|
||||
.unwrap_or_else(|| {
|
||||
dirs::cache_dir().map_or_else(|| output_path.to_path_buf(), |p| p.join("tauri"))
|
||||
});
|
||||
|
||||
fs::create_dir_all(&tools_path)?;
|
||||
|
||||
let linuxdeploy_path = prepare_tools(
|
||||
&tools_path,
|
||||
tools_arch,
|
||||
settings.log_level() != log::Level::Error,
|
||||
)?;
|
||||
|
||||
let package_dir = settings.project_out_directory().join("bundle/appimage_deb");
|
||||
|
||||
let main_binary = settings.main_binary()?;
|
||||
let product_name = settings.product_name();
|
||||
|
||||
let mut settings = settings.clone();
|
||||
if main_binary.name().contains(' ') {
|
||||
let main_binary_path = settings.binary_path(main_binary);
|
||||
let project_out_directory = settings.project_out_directory();
|
||||
|
||||
let main_binary_name_kebab = heck::AsKebabCase(main_binary.name()).to_string();
|
||||
let new_path = project_out_directory.join(&main_binary_name_kebab);
|
||||
fs::copy(main_binary_path, new_path)?;
|
||||
|
||||
let main_binary = settings.main_binary_mut()?;
|
||||
main_binary.set_name(main_binary_name_kebab);
|
||||
}
|
||||
|
||||
// generate deb_folder structure
|
||||
let (data_dir, icons) = debian::generate_data(&settings, &package_dir)
|
||||
.with_context(|| "Failed to build data folders and files")?;
|
||||
fs_utils::copy_custom_files(&settings.appimage().files, &data_dir)
|
||||
.with_context(|| "Failed to copy custom files")?;
|
||||
|
||||
fs::create_dir_all(&output_path)?;
|
||||
let app_dir_path = output_path.join(format!("{}.AppDir", settings.product_name()));
|
||||
let appimage_filename = format!(
|
||||
"{}_{}_{}.AppImage",
|
||||
settings.product_name(),
|
||||
settings.version_string(),
|
||||
appimage_arch
|
||||
);
|
||||
let appimage_path = output_path.join(&appimage_filename);
|
||||
fs_utils::create_dir(&app_dir_path, true)?;
|
||||
|
||||
fs::create_dir_all(&tools_path)?;
|
||||
let larger_icon = icons
|
||||
.iter()
|
||||
.filter(|i| i.width == i.height)
|
||||
.max_by_key(|i| i.width)
|
||||
.expect("couldn't find a square icon to use as AppImage icon");
|
||||
let larger_icon_path = larger_icon
|
||||
.path
|
||||
.strip_prefix(package_dir.join("data"))
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
log::info!(action = "Bundling"; "{} ({})", appimage_filename, appimage_path.display());
|
||||
|
||||
let app_dir_usr = app_dir_path.join("usr/");
|
||||
let app_dir_usr_bin = app_dir_usr.join("bin/");
|
||||
let app_dir_usr_lib = app_dir_usr.join("lib/");
|
||||
|
||||
fs_utils::copy_dir(&data_dir.join("usr/"), &app_dir_usr)?;
|
||||
|
||||
// Using create_dir_all for a single dir so we don't get errors if the path already exists
|
||||
fs::create_dir_all(&app_dir_usr_bin)?;
|
||||
fs::create_dir_all(app_dir_usr_lib)?;
|
||||
|
||||
// Copy bins and libs that linuxdeploy doesn't know about
|
||||
|
||||
// we also check if the user may have provided their own copy already
|
||||
// xdg-open will be handled by the `files` config instead
|
||||
if settings.deep_link_protocols().is_some() && !app_dir_usr_bin.join("xdg-open").exists() {
|
||||
fs::copy("/usr/bin/xdg-mime", app_dir_usr_bin.join("xdg-mime"))
|
||||
.fs_context("xdg-mime binary not found", "/usr/bin/xdg-mime".to_string())?;
|
||||
}
|
||||
|
||||
// we also check if the user may have provided their own copy already
|
||||
if settings.appimage().bundle_xdg_open && !app_dir_usr_bin.join("xdg-open").exists() {
|
||||
fs::copy("/usr/bin/xdg-open", app_dir_usr_bin.join("xdg-open"))
|
||||
.fs_context("xdg-open binary not found", "/usr/bin/xdg-open".to_string())?;
|
||||
}
|
||||
|
||||
let search_dirs = [
|
||||
match settings.binary_arch() {
|
||||
Arch::X86_64 => "/usr/lib/x86_64-linux-gnu/",
|
||||
Arch::X86 => "/usr/lib/i386-linux-gnu/",
|
||||
Arch::AArch64 => "/usr/lib/aarch64-linux-gnu/",
|
||||
Arch::Armhf => "/usr/lib/arm-linux-gnueabihf/",
|
||||
_ => unreachable!(),
|
||||
},
|
||||
"/usr/lib64",
|
||||
"/usr/lib",
|
||||
"/usr/libexec",
|
||||
];
|
||||
|
||||
for file in [
|
||||
"WebKitNetworkProcess",
|
||||
"WebKitWebProcess",
|
||||
"injected-bundle/libwebkit2gtkinjectedbundle.so",
|
||||
] {
|
||||
for source in search_dirs.map(PathBuf::from) {
|
||||
// TODO: Check if it's the same dir name on all systems
|
||||
let source = source.join("webkit2gtk-4.1").join(file);
|
||||
if source.exists() {
|
||||
fs_utils::copy_file(
|
||||
&source,
|
||||
&app_dir_path.join(source.strip_prefix("/").unwrap()),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs::copy(
|
||||
tools_path.join(format!("AppRun-{tools_arch}")),
|
||||
app_dir_path.join("AppRun"),
|
||||
)?;
|
||||
fs::copy(
|
||||
app_dir_path.join(larger_icon_path),
|
||||
app_dir_path.join(format!("{product_name}.png")),
|
||||
)?;
|
||||
std::os::unix::fs::symlink(
|
||||
app_dir_path.join(format!("{product_name}.png")),
|
||||
app_dir_path.join(".DirIcon"),
|
||||
)?;
|
||||
std::os::unix::fs::symlink(
|
||||
app_dir_path.join(format!("usr/share/applications/{product_name}.desktop")),
|
||||
app_dir_path.join(format!("{product_name}.desktop")),
|
||||
)?;
|
||||
|
||||
let log_level = match settings.log_level() {
|
||||
log::Level::Error => "3",
|
||||
log::Level::Warn => "2",
|
||||
log::Level::Info => "1",
|
||||
_ => "0",
|
||||
};
|
||||
|
||||
let mut cmd = Command::new(linuxdeploy_path);
|
||||
cmd.env("OUTPUT", &appimage_path);
|
||||
cmd.env("ARCH", tools_arch);
|
||||
// Looks like the cli arg isn't enough for the updated AppImage output-plugin.
|
||||
cmd.env("APPIMAGE_EXTRACT_AND_RUN", "1");
|
||||
cmd.args([
|
||||
"--appimage-extract-and-run",
|
||||
"--verbosity",
|
||||
log_level,
|
||||
"--appdir",
|
||||
&app_dir_path.display().to_string(),
|
||||
"--plugin",
|
||||
"gtk",
|
||||
]);
|
||||
if settings.appimage().bundle_media_framework {
|
||||
cmd.args(["--plugin", "gstreamer"]);
|
||||
}
|
||||
cmd.args(["--output", "appimage"]);
|
||||
|
||||
// Linuxdeploy logs everything into stderr so we have to ignore the output ourselves here
|
||||
if settings.log_level() == log::Level::Error {
|
||||
log::debug!(action = "Running"; "Command `linuxdeploy {}`", cmd.get_args().map(|arg| arg.to_string_lossy()).fold(String::new(), |acc, arg| format!("{acc} {arg}")));
|
||||
if !cmd.output()?.status.success() {
|
||||
return Err(crate::Error::GenericError(
|
||||
"failed to run linuxdeploy".to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
cmd.output_ok()?;
|
||||
}
|
||||
|
||||
fs::remove_dir_all(&package_dir)?;
|
||||
Ok(vec![appimage_path])
|
||||
}
|
||||
|
||||
// returns the linuxdeploy path to keep linuxdeploy_arch contained
|
||||
fn prepare_tools(tools_path: &Path, arch: &str, verbose: bool) -> crate::Result<PathBuf> {
|
||||
let apprun = tools_path.join(format!("AppRun-{arch}"));
|
||||
if !apprun.exists() {
|
||||
let data = download(&format!(
|
||||
"https://github.com/tauri-apps/binary-releases/releases/download/apprun-old/AppRun-{arch}"
|
||||
))?;
|
||||
write_and_make_executable(&apprun, data)?;
|
||||
}
|
||||
|
||||
let linuxdeploy_arch = if arch == "i686" { "i386" } else { arch };
|
||||
let linuxdeploy = tools_path.join(format!("linuxdeploy-{linuxdeploy_arch}.AppImage"));
|
||||
if !linuxdeploy.exists() {
|
||||
let data = download(&format!("https://github.com/tauri-apps/binary-releases/releases/download/linuxdeploy/linuxdeploy-{linuxdeploy_arch}.AppImage"))?;
|
||||
write_and_make_executable(&linuxdeploy, data)?;
|
||||
}
|
||||
|
||||
let gtk = tools_path.join("linuxdeploy-plugin-gtk.sh");
|
||||
if !gtk.exists() {
|
||||
let data = download("https://raw.githubusercontent.com/tauri-apps/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh")?;
|
||||
write_and_make_executable(>k, data)?;
|
||||
}
|
||||
|
||||
let gstreamer = tools_path.join("linuxdeploy-plugin-gstreamer.sh");
|
||||
if !gstreamer.exists() {
|
||||
let data = download("https://raw.githubusercontent.com/tauri-apps/linuxdeploy-plugin-gstreamer/master/linuxdeploy-plugin-gstreamer.sh")?;
|
||||
write_and_make_executable(&gstreamer, data)?;
|
||||
}
|
||||
|
||||
let appimage = tools_path.join("linuxdeploy-plugin-appimage.AppImage");
|
||||
if !appimage.exists() {
|
||||
// This is optional, linuxdeploy will fall back to its built-in version if the download failed.
|
||||
let data = download(&format!("https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-{arch}.AppImage"));
|
||||
match data {
|
||||
Ok(data) => write_and_make_executable(&appimage, data)?,
|
||||
Err(err) => {
|
||||
log::error!("Download of AppImage plugin failed. Using older built-in version instead.");
|
||||
if verbose {
|
||||
log::debug!("{err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This should prevent linuxdeploy to be detected by appimage integration tools
|
||||
let _ = Command::new("dd")
|
||||
.args([
|
||||
"if=/dev/zero",
|
||||
"bs=1",
|
||||
"count=3",
|
||||
"seek=8",
|
||||
"conv=notrunc",
|
||||
&format!("of={}", linuxdeploy.display()),
|
||||
])
|
||||
.output();
|
||||
|
||||
Ok(linuxdeploy)
|
||||
}
|
||||
@@ -1,124 +1,25 @@
|
||||
// Copyright 2016-2019 Cargo-Bundle developers <https://github.com/burtonageo/cargo-bundle>
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use super::{
|
||||
super::{
|
||||
common::{self, CommandExt},
|
||||
path_utils,
|
||||
},
|
||||
debian,
|
||||
};
|
||||
use crate::{bundle::settings::Arch, Settings};
|
||||
use anyhow::Context;
|
||||
use handlebars::Handlebars;
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
fs::{remove_dir_all, write},
|
||||
path::PathBuf,
|
||||
process::{Command, Stdio},
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
/// Bundles the project.
|
||||
/// Returns a vector of PathBuf that shows where the AppImage was created.
|
||||
use crate::Settings;
|
||||
|
||||
mod linuxdeploy;
|
||||
|
||||
pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
// generate the deb binary name
|
||||
let arch: &str = match settings.binary_arch() {
|
||||
Arch::X86_64 => "amd64",
|
||||
Arch::X86 => "i386",
|
||||
Arch::AArch64 => "aarch64",
|
||||
Arch::Armhf => "armhf",
|
||||
target => {
|
||||
return Err(crate::Error::ArchError(format!(
|
||||
"Unsupported architecture: {:?}",
|
||||
target
|
||||
)));
|
||||
}
|
||||
};
|
||||
let package_dir = settings.project_out_directory().join("bundle/appimage_deb");
|
||||
|
||||
// generate deb_folder structure
|
||||
let (data_dir, icons) = debian::generate_data(settings, &package_dir)
|
||||
.with_context(|| "Failed to build data folders and files")?;
|
||||
common::copy_custom_files(&settings.appimage().files, &data_dir)
|
||||
.with_context(|| "Failed to copy custom files")?;
|
||||
|
||||
let output_path = settings.project_out_directory().join("bundle/appimage");
|
||||
if output_path.exists() {
|
||||
remove_dir_all(&output_path)?;
|
||||
}
|
||||
std::fs::create_dir_all(output_path.clone())?;
|
||||
let app_dir_path = output_path.join(format!("{}.AppDir", settings.product_name()));
|
||||
let appimage_filename = format!(
|
||||
"{}_{}_{}.AppImage",
|
||||
settings.product_name(),
|
||||
settings.version_string(),
|
||||
arch
|
||||
);
|
||||
let appimage_path = output_path.join(&appimage_filename);
|
||||
path_utils::create(app_dir_path, true)?;
|
||||
|
||||
// setup data to insert into shell script
|
||||
let mut sh_map = BTreeMap::new();
|
||||
sh_map.insert("arch", settings.target().split('-').next().unwrap());
|
||||
sh_map.insert("product_name", settings.product_name());
|
||||
sh_map.insert("appimage_filename", &appimage_filename);
|
||||
|
||||
let tauri_tools_path = settings
|
||||
.local_tools_directory()
|
||||
.map(|d| d.join(".tauri"))
|
||||
.unwrap_or_else(|| {
|
||||
dirs::cache_dir().map_or_else(|| output_path.to_path_buf(), |p| p.join("tauri"))
|
||||
});
|
||||
|
||||
std::fs::create_dir_all(&tauri_tools_path)?;
|
||||
let tauri_tools_path_str = tauri_tools_path.to_string_lossy();
|
||||
sh_map.insert("tauri_tools_path", &tauri_tools_path_str);
|
||||
let larger_icon = icons
|
||||
.iter()
|
||||
.filter(|i| i.width == i.height)
|
||||
.max_by_key(|i| i.width)
|
||||
.expect("couldn't find a square icon to use as AppImage icon");
|
||||
let larger_icon_path = larger_icon
|
||||
.path
|
||||
.strip_prefix(package_dir.join("data"))
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
sh_map.insert("icon_path", &larger_icon_path);
|
||||
|
||||
// initialize shell script template.
|
||||
let mut handlebars = Handlebars::new();
|
||||
handlebars.register_escape_fn(handlebars::no_escape);
|
||||
handlebars
|
||||
.register_template_string("appimage", include_str!("./appimage"))
|
||||
.expect("Failed to register template for handlebars");
|
||||
let temp = handlebars.render("appimage", &sh_map)?;
|
||||
|
||||
// create the shell script file in the target/ folder.
|
||||
let sh_file = output_path.join("build_appimage.sh");
|
||||
|
||||
log::info!(action = "Bundling"; "{} ({})", appimage_filename, appimage_path.display());
|
||||
|
||||
write(&sh_file, temp)?;
|
||||
|
||||
// chmod script for execution
|
||||
Command::new("chmod")
|
||||
.arg("777")
|
||||
.arg(&sh_file)
|
||||
.current_dir(output_path.clone())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.expect("Failed to chmod script");
|
||||
|
||||
// execute the shell script to build the appimage.
|
||||
Command::new(&sh_file)
|
||||
.current_dir(output_path)
|
||||
.output_ok()
|
||||
.context("error running build_appimage.sh")?;
|
||||
|
||||
remove_dir_all(&package_dir)?;
|
||||
Ok(vec![appimage_path])
|
||||
linuxdeploy::bundle_project(settings)
|
||||
}
|
||||
|
||||
fn write_and_make_executable(path: &Path, data: Vec<u8>) -> std::io::Result<()> {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
fs::write(path, data)?;
|
||||
fs::set_permissions(path, fs::Permissions::from_mode(0o770))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -23,9 +23,13 @@
|
||||
// metadata, as well as generating the md5sums file. Currently we do not
|
||||
// generate postinst or prerm files.
|
||||
|
||||
use super::{super::common, freedesktop};
|
||||
use crate::{bundle::settings::Arch, Settings};
|
||||
use anyhow::Context;
|
||||
use super::freedesktop;
|
||||
use crate::{
|
||||
bundle::settings::Arch,
|
||||
error::{Context, ErrorExt},
|
||||
utils::fs_utils,
|
||||
Settings,
|
||||
};
|
||||
use flate2::{write::GzEncoder, Compression};
|
||||
use tar::HeaderMode;
|
||||
use walkdir::WalkDir;
|
||||
@@ -46,10 +50,10 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
Arch::AArch64 => "arm64",
|
||||
Arch::Armhf => "armhf",
|
||||
Arch::Armel => "armel",
|
||||
Arch::Riscv64 => "riscv64",
|
||||
target => {
|
||||
return Err(crate::Error::ArchError(format!(
|
||||
"Unsupported architecture: {:?}",
|
||||
target
|
||||
"Unsupported architecture: {target:?}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
@@ -64,30 +68,32 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
let base_dir = settings.project_out_directory().join("bundle/deb");
|
||||
let package_dir = base_dir.join(&package_base_name);
|
||||
if package_dir.exists() {
|
||||
fs::remove_dir_all(&package_dir)
|
||||
.with_context(|| format!("Failed to remove old {package_base_name}"))?;
|
||||
fs::remove_dir_all(&package_dir).fs_context(
|
||||
"Failed to Remove old package directory",
|
||||
package_dir.clone(),
|
||||
)?;
|
||||
}
|
||||
let package_path = base_dir.join(&package_name);
|
||||
|
||||
log::info!(action = "Bundling"; "{} ({})", package_name, package_path.display());
|
||||
|
||||
let (data_dir, _) = generate_data(settings, &package_dir)
|
||||
.with_context(|| "Failed to build data folders and files")?;
|
||||
common::copy_custom_files(&settings.deb().files, &data_dir)
|
||||
.with_context(|| "Failed to copy custom files")?;
|
||||
let (data_dir, _) =
|
||||
generate_data(settings, &package_dir).context("Failed to build data folders and files")?;
|
||||
fs_utils::copy_custom_files(&settings.deb().files, &data_dir)
|
||||
.context("Failed to copy custom files")?;
|
||||
|
||||
// Generate control files.
|
||||
let control_dir = package_dir.join("control");
|
||||
generate_control_file(settings, arch, &control_dir, &data_dir)
|
||||
.with_context(|| "Failed to create control file")?;
|
||||
generate_scripts(settings, &control_dir).with_context(|| "Failed to create control scripts")?;
|
||||
generate_md5sums(&control_dir, &data_dir).with_context(|| "Failed to create md5sums file")?;
|
||||
.context("Failed to create control file")?;
|
||||
generate_scripts(settings, &control_dir).context("Failed to create control scripts")?;
|
||||
generate_md5sums(&control_dir, &data_dir).context("Failed to create md5sums file")?;
|
||||
|
||||
// Generate `debian-binary` file; see
|
||||
// http://www.tldp.org/HOWTO/Debian-Binary-Package-Building-HOWTO/x60.html#AEN66
|
||||
let debian_binary_path = package_dir.join("debian-binary");
|
||||
create_file_with_data(&debian_binary_path, "2.0\n")
|
||||
.with_context(|| "Failed to create debian-binary file")?;
|
||||
.context("Failed to create debian-binary file")?;
|
||||
|
||||
// Apply tar/gzip/ar to create the final package file.
|
||||
let control_tar_gz_path =
|
||||
@@ -113,8 +119,9 @@ pub fn generate_data(
|
||||
|
||||
for bin in settings.binaries() {
|
||||
let bin_path = settings.binary_path(bin);
|
||||
common::copy_file(&bin_path, bin_dir.join(bin.name()))
|
||||
.with_context(|| format!("Failed to copy binary from {bin_path:?}"))?;
|
||||
let trgt = bin_dir.join(bin.name());
|
||||
fs_utils::copy_file(&bin_path, &trgt)
|
||||
.with_context(|| format!("Failed to copy binary from {bin_path:?} to {trgt:?}"))?;
|
||||
}
|
||||
|
||||
copy_resource_files(settings, &data_dir).with_context(|| "Failed to copy resource files")?;
|
||||
@@ -141,7 +148,7 @@ fn generate_changelog_file(settings: &Settings, data_dir: &Path) -> crate::Resul
|
||||
let product_name = settings.product_name();
|
||||
let dest_path = data_dir.join(format!("usr/share/doc/{product_name}/changelog.gz"));
|
||||
|
||||
let changelog_file = common::create_file(&dest_path)?;
|
||||
let changelog_file = fs_utils::create_file(&dest_path)?;
|
||||
let mut gzip_encoder = GzEncoder::new(changelog_file, Compression::new(9));
|
||||
io::copy(&mut src_file, &mut gzip_encoder)?;
|
||||
|
||||
@@ -161,9 +168,9 @@ fn generate_control_file(
|
||||
// For more information about the format of this file, see
|
||||
// https://www.debian.org/doc/debian-policy/ch-controlfields.html
|
||||
let dest_path = control_dir.join("control");
|
||||
let mut file = common::create_file(&dest_path)?;
|
||||
let mut file = fs_utils::create_file(&dest_path)?;
|
||||
let package = heck::AsKebabCase(settings.product_name());
|
||||
writeln!(file, "Package: {}", package)?;
|
||||
writeln!(file, "Package: {package}")?;
|
||||
writeln!(file, "Version: {}", settings.version_string())?;
|
||||
writeln!(file, "Architecture: {arch}")?;
|
||||
// Installed-Size must be divided by 1024, see https://www.debian.org/doc/debian-policy/ch-controlfields.html#installed-size
|
||||
@@ -182,22 +189,31 @@ fn generate_control_file(
|
||||
|
||||
writeln!(file, "Maintainer: {authors}")?;
|
||||
if let Some(section) = &settings.deb().section {
|
||||
writeln!(file, "Section: {}", section)?;
|
||||
writeln!(file, "Section: {section}")?;
|
||||
}
|
||||
if let Some(priority) = &settings.deb().priority {
|
||||
writeln!(file, "Priority: {}", priority)?;
|
||||
writeln!(file, "Priority: {priority}")?;
|
||||
} else {
|
||||
writeln!(file, "Priority: optional")?;
|
||||
}
|
||||
|
||||
if let Some(homepage) = settings.homepage_url() {
|
||||
writeln!(file, "Homepage: {}", homepage)?;
|
||||
writeln!(file, "Homepage: {homepage}")?;
|
||||
}
|
||||
|
||||
let dependencies = settings.deb().depends.as_ref().cloned().unwrap_or_default();
|
||||
if !dependencies.is_empty() {
|
||||
writeln!(file, "Depends: {}", dependencies.join(", "))?;
|
||||
}
|
||||
let dependencies = settings
|
||||
.deb()
|
||||
.recommends
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
if !dependencies.is_empty() {
|
||||
writeln!(file, "Recommends: {}", dependencies.join(", "))?;
|
||||
}
|
||||
let provides = settings
|
||||
.deb()
|
||||
.provides
|
||||
@@ -285,7 +301,7 @@ fn create_script_file_from_path(from: &PathBuf, to: &PathBuf) -> crate::Result<(
|
||||
/// for each file within the `data_dir`.
|
||||
fn generate_md5sums(control_dir: &Path, data_dir: &Path) -> crate::Result<()> {
|
||||
let md5sums_path = control_dir.join("md5sums");
|
||||
let mut md5sums_file = common::create_file(&md5sums_path)?;
|
||||
let mut md5sums_file = fs_utils::create_file(&md5sums_path)?;
|
||||
for entry in WalkDir::new(data_dir) {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
@@ -295,7 +311,7 @@ fn generate_md5sums(control_dir: &Path, data_dir: &Path) -> crate::Result<()> {
|
||||
let mut file = File::open(path)?;
|
||||
let mut hash = md5::Context::new();
|
||||
io::copy(&mut file, &mut hash)?;
|
||||
for byte in hash.compute().iter() {
|
||||
for byte in hash.finalize().iter() {
|
||||
write!(md5sums_file, "{byte:02x}")?;
|
||||
}
|
||||
let rel_path = path.strip_prefix(data_dir)?;
|
||||
@@ -318,7 +334,7 @@ fn copy_resource_files(settings: &Settings, data_dir: &Path) -> crate::Result<()
|
||||
/// Create an empty file at the given path, creating any parent directories as
|
||||
/// needed, then write `data` into the file.
|
||||
fn create_file_with_data<P: AsRef<Path>>(path: P, data: &str) -> crate::Result<()> {
|
||||
let mut file = common::create_file(path.as_ref())?;
|
||||
let mut file = fs_utils::create_file(path.as_ref())?;
|
||||
file.write_all(data.as_bytes())?;
|
||||
file.flush()?;
|
||||
Ok(())
|
||||
@@ -367,7 +383,7 @@ fn create_tar_from_dir<P: AsRef<Path>, W: Write>(src_dir: P, dest_file: W) -> cr
|
||||
fn tar_and_gzip_dir<P: AsRef<Path>>(src_dir: P) -> crate::Result<PathBuf> {
|
||||
let src_dir = src_dir.as_ref();
|
||||
let dest_path = src_dir.with_extension("tar.gz");
|
||||
let dest_file = common::create_file(&dest_path)?;
|
||||
let dest_file = fs_utils::create_file(&dest_path)?;
|
||||
let gzip_encoder = GzEncoder::new(dest_file, Compression::default());
|
||||
let gzip_encoder = create_tar_from_dir(src_dir, gzip_encoder)?;
|
||||
let mut dest_file = gzip_encoder.finish()?;
|
||||
@@ -378,7 +394,7 @@ fn tar_and_gzip_dir<P: AsRef<Path>>(src_dir: P) -> crate::Result<PathBuf> {
|
||||
/// Creates an `ar` archive from the given source files and writes it to the
|
||||
/// given destination path.
|
||||
fn create_archive(srcs: Vec<PathBuf>, dest: &Path) -> crate::Result<()> {
|
||||
let mut builder = ar::Builder::new(common::create_file(dest)?);
|
||||
let mut builder = ar::Builder::new(fs_utils::create_file(dest)?);
|
||||
for path in &srcs {
|
||||
builder.append_path(path)?;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ Categories={{categories}}
|
||||
Comment={{comment}}
|
||||
{{/if}}
|
||||
Exec={{exec}}
|
||||
StartupWMClass={{exec}}
|
||||
Icon={{icon}}
|
||||
Name={{name}}
|
||||
Terminal=false
|
||||
|
||||
@@ -21,13 +21,15 @@ use std::fs::{read_to_string, File};
|
||||
use std::io::BufReader;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::Context;
|
||||
use handlebars::Handlebars;
|
||||
use image::{self, codecs::png::PngDecoder, ImageDecoder};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::bundle::common;
|
||||
use crate::Settings;
|
||||
use crate::{
|
||||
error::Context,
|
||||
utils::{self, fs_utils},
|
||||
Settings,
|
||||
};
|
||||
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Icon {
|
||||
@@ -65,7 +67,7 @@ pub fn list_icon_files(
|
||||
let decoder = PngDecoder::new(BufReader::new(File::open(&icon_path)?))?;
|
||||
let width = decoder.dimensions().0;
|
||||
let height = decoder.dimensions().1;
|
||||
let is_high_density = common::is_retina(&icon_path);
|
||||
let is_high_density = utils::is_retina(&icon_path);
|
||||
let dest_path = get_dest_path(width, height, is_high_density);
|
||||
Icon {
|
||||
width,
|
||||
@@ -84,7 +86,7 @@ pub fn list_icon_files(
|
||||
pub fn copy_icon_files(settings: &Settings, data_dir: &Path) -> crate::Result<Vec<Icon>> {
|
||||
let icons = list_icon_files(settings, data_dir)?;
|
||||
for (icon, src) in &icons {
|
||||
common::copy_file(src, &icon.path)?;
|
||||
fs_utils::copy_file(src, &icon.path)?;
|
||||
}
|
||||
|
||||
Ok(icons.into_keys().collect())
|
||||
@@ -99,23 +101,26 @@ pub fn generate_desktop_file(
|
||||
data_dir: &Path,
|
||||
) -> crate::Result<(PathBuf, PathBuf)> {
|
||||
let bin_name = settings.main_binary_name()?;
|
||||
|
||||
let product_name = settings.product_name();
|
||||
let desktop_file_name = format!("{product_name}.desktop");
|
||||
let path = PathBuf::from("usr/share/applications").join(desktop_file_name);
|
||||
let dest_path = PathBuf::from("/").join(&path);
|
||||
let file_path = data_dir.join(&path);
|
||||
let file = &mut common::create_file(&file_path)?;
|
||||
let file = &mut fs_utils::create_file(&file_path)?;
|
||||
|
||||
let mut handlebars = Handlebars::new();
|
||||
handlebars.register_escape_fn(handlebars::no_escape);
|
||||
if let Some(template) = custom_template_path {
|
||||
handlebars
|
||||
.register_template_string("main.desktop", read_to_string(template)?)
|
||||
.with_context(|| "Failed to setup custom handlebar template")?;
|
||||
.map_err(Into::into)
|
||||
.context("Failed to setup custom handlebar template")?;
|
||||
} else {
|
||||
handlebars
|
||||
.register_template_string("main.desktop", include_str!("./main.desktop"))
|
||||
.with_context(|| "Failed to setup default handlebar template")?;
|
||||
.map_err(Into::into)
|
||||
.context("Failed to setup default handlebar template")?;
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -150,6 +155,12 @@ pub fn generate_desktop_file(
|
||||
|
||||
let mime_type = (!mime_type.is_empty()).then_some(mime_type.join(";"));
|
||||
|
||||
let bin_name_exec = if bin_name.contains(' ') {
|
||||
format!("\"{bin_name}\"")
|
||||
} else {
|
||||
bin_name.to_string()
|
||||
};
|
||||
|
||||
handlebars.render_to_write(
|
||||
"main.desktop",
|
||||
&DesktopTemplateParams {
|
||||
@@ -162,7 +173,7 @@ pub fn generate_desktop_file(
|
||||
} else {
|
||||
None
|
||||
},
|
||||
exec: bin_name,
|
||||
exec: &bin_name_exec,
|
||||
icon: bin_name,
|
||||
name: settings.product_name(),
|
||||
mime_type,
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::{bundle::settings::Arch, Settings};
|
||||
use crate::{bundle::settings::Arch, error::ErrorExt, Settings};
|
||||
|
||||
use anyhow::Context;
|
||||
use rpm::{self, signature::pgp, Dependency, FileMode, FileOptions};
|
||||
use std::{
|
||||
env,
|
||||
fs::{self, File},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use tauri_utils::config::RpmCompression;
|
||||
|
||||
use super::freedesktop;
|
||||
|
||||
@@ -20,7 +20,10 @@ use super::freedesktop;
|
||||
pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
let product_name = settings.product_name();
|
||||
let version = settings.version_string();
|
||||
let release = settings.rpm().release.as_str();
|
||||
let release = match settings.rpm().release.as_str() {
|
||||
"" => "1", // Considered the default. If left empty, you get file with "-.".
|
||||
v => v,
|
||||
};
|
||||
let epoch = settings.rpm().epoch;
|
||||
let arch = match settings.binary_arch() {
|
||||
Arch::X86_64 => "x86_64",
|
||||
@@ -28,10 +31,10 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
Arch::AArch64 => "aarch64",
|
||||
Arch::Armhf => "armhfp",
|
||||
Arch::Armel => "armel",
|
||||
Arch::Riscv64 => "riscv64",
|
||||
target => {
|
||||
return Err(crate::Error::ArchError(format!(
|
||||
"Unsupported architecture: {:?}",
|
||||
target
|
||||
"Unsupported architecture: {target:?}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
@@ -44,21 +47,38 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
let base_dir = settings.project_out_directory().join("bundle/rpm");
|
||||
let package_dir = base_dir.join(&package_base_name);
|
||||
if package_dir.exists() {
|
||||
fs::remove_dir_all(&package_dir)
|
||||
.with_context(|| format!("Failed to remove old {package_base_name}"))?;
|
||||
fs::remove_dir_all(&package_dir).fs_context(
|
||||
"Failed to remove old package directory",
|
||||
package_dir.clone(),
|
||||
)?;
|
||||
}
|
||||
fs::create_dir_all(&package_dir)?;
|
||||
fs::create_dir_all(&package_dir)
|
||||
.fs_context("Failed to create package directory", package_dir.clone())?;
|
||||
let package_path = base_dir.join(&package_name);
|
||||
|
||||
log::info!(action = "Bundling"; "{} ({})", package_name, package_path.display());
|
||||
|
||||
let license = settings.license().unwrap_or_default();
|
||||
let name = heck::AsKebabCase(settings.product_name()).to_string();
|
||||
|
||||
let compression = settings
|
||||
.rpm()
|
||||
.compression
|
||||
.map(|c| match c {
|
||||
RpmCompression::Gzip { level } => rpm::CompressionWithLevel::Gzip(level),
|
||||
RpmCompression::Zstd { level } => rpm::CompressionWithLevel::Zstd(level),
|
||||
RpmCompression::Xz { level } => rpm::CompressionWithLevel::Xz(level),
|
||||
RpmCompression::Bzip2 { level } => rpm::CompressionWithLevel::Bzip2(level),
|
||||
_ => rpm::CompressionWithLevel::None,
|
||||
})
|
||||
// This matches .deb compression. On a 240MB source binary the bundle will be 100KB larger than rpm's default while reducing build times by ~25%.
|
||||
// TODO: Default to Zstd in v3 to match rpm-rs new default in 0.16
|
||||
.unwrap_or(rpm::CompressionWithLevel::Gzip(6));
|
||||
|
||||
let mut builder = rpm::PackageBuilder::new(&name, version, &license, arch, summary)
|
||||
.epoch(epoch)
|
||||
.release(release)
|
||||
// This matches .deb compression. On a 240MB source binary the bundle will be 100KB larger than rpm's default while reducing build times by ~25%.
|
||||
.compression(rpm::CompressionWithLevel::Gzip(6));
|
||||
.compression(compression);
|
||||
|
||||
if let Some(description) = settings.long_description() {
|
||||
builder = builder.description(description);
|
||||
@@ -84,6 +104,17 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
builder = builder.provides(Dependency::any(dep));
|
||||
}
|
||||
|
||||
// Add recommends
|
||||
for dep in settings
|
||||
.rpm()
|
||||
.recommends
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
{
|
||||
builder = builder.recommends(Dependency::any(dep));
|
||||
}
|
||||
|
||||
// Add conflicts
|
||||
for dep in settings
|
||||
.rpm()
|
||||
@@ -160,10 +191,10 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
FileOptions::new(resource_dir.to_string_lossy()).mode(FileMode::Dir { permissions: 0o755 }),
|
||||
)?;
|
||||
// Then add the resources files in that directory
|
||||
for src in settings.resource_files() {
|
||||
let src = src?;
|
||||
let dest = resource_dir.join(tauri_utils::resources::resource_relpath(&src));
|
||||
builder = builder.with_file(&src, FileOptions::new(dest.to_string_lossy()))?;
|
||||
for resource in settings.resource_files().iter() {
|
||||
let resource = resource?;
|
||||
let dest = resource_dir.join(resource.target());
|
||||
builder = builder.with_file(resource.path(), FileOptions::new(dest.to_string_lossy()))?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,6 +239,5 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
|
||||
let mut f = fs::File::create(&package_path)?;
|
||||
pkg.write(&mut f)?;
|
||||
|
||||
Ok(vec![package_path])
|
||||
}
|
||||
|
||||
@@ -23,13 +23,16 @@
|
||||
// files into the `Contents` directory of the bundle.
|
||||
|
||||
use super::{
|
||||
super::common::{self, CommandExt},
|
||||
icon::create_icns_file,
|
||||
sign::{notarize, notarize_auth, sign, NotarizeAuthError, SignTarget},
|
||||
icon::{app_icon_name_from_assets_car, create_assets_car_file, create_icns_file},
|
||||
sign::{notarize, notarize_auth, notarize_without_stapling, sign, SignTarget},
|
||||
};
|
||||
use crate::{
|
||||
bundle::settings::PlistKind,
|
||||
error::{Context, ErrorExt, NotarizeAuthError},
|
||||
utils::{fs_utils, CommandExt},
|
||||
Error::GenericError,
|
||||
Settings,
|
||||
};
|
||||
use crate::Settings;
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
@@ -63,25 +66,29 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
|
||||
if app_bundle_path.exists() {
|
||||
fs::remove_dir_all(&app_bundle_path)
|
||||
.with_context(|| format!("Failed to remove old {}", app_product_name))?;
|
||||
.fs_context("failed to remove old app bundle", &app_bundle_path)?;
|
||||
}
|
||||
let bundle_directory = app_bundle_path.join("Contents");
|
||||
fs::create_dir_all(&bundle_directory).with_context(|| {
|
||||
format!(
|
||||
"Failed to create bundle directory at {:?}",
|
||||
bundle_directory
|
||||
)
|
||||
})?;
|
||||
fs::create_dir_all(&bundle_directory)
|
||||
.fs_context("failed to create bundle directory", &bundle_directory)?;
|
||||
|
||||
let resources_dir = bundle_directory.join("Resources");
|
||||
let bin_dir = bundle_directory.join("MacOS");
|
||||
let mut sign_paths = Vec::new();
|
||||
|
||||
let bundle_icon_file: Option<PathBuf> =
|
||||
{ create_icns_file(&resources_dir, settings).with_context(|| "Failed to create app icon")? };
|
||||
let bundle_icon_file =
|
||||
create_icns_file(&resources_dir, settings).with_context(|| "Failed to create app icon")?;
|
||||
|
||||
create_info_plist(&bundle_directory, bundle_icon_file, settings)
|
||||
.with_context(|| "Failed to create Info.plist")?;
|
||||
let assets_car_file = create_assets_car_file(&resources_dir, settings)
|
||||
.with_context(|| "Failed to create app Assets.car")?;
|
||||
|
||||
create_info_plist(
|
||||
&bundle_directory,
|
||||
bundle_icon_file,
|
||||
assets_car_file,
|
||||
settings,
|
||||
)
|
||||
.with_context(|| "Failed to create Info.plist")?;
|
||||
|
||||
let framework_paths = copy_frameworks_to_bundle(&bundle_directory, settings)
|
||||
.with_context(|| "Failed to bundle frameworks")?;
|
||||
@@ -105,7 +112,11 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
|
||||
copy_custom_files_to_bundle(&bundle_directory, settings)?;
|
||||
|
||||
if let Some(keychain) = super::sign::keychain(settings.macos().signing_identity.as_deref())? {
|
||||
if settings.no_sign() {
|
||||
log::warn!("Skipping signing due to --no-sign flag.",);
|
||||
} else if let Some(keychain) =
|
||||
super::sign::keychain(settings.macos().signing_identity.as_deref())?
|
||||
{
|
||||
// Sign frameworks and sidecar binaries first, per apple, signing must be done inside out
|
||||
// https://developer.apple.com/forums/thread/701514
|
||||
sign_paths.push(SignTarget {
|
||||
@@ -123,13 +134,17 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
// notarization is required for distribution
|
||||
match notarize_auth() {
|
||||
Ok(auth) => {
|
||||
notarize(&keychain, app_bundle_path.clone(), &auth)?;
|
||||
if settings.macos().skip_stapling {
|
||||
notarize_without_stapling(&keychain, app_bundle_path.clone(), &auth)?;
|
||||
} else {
|
||||
notarize(&keychain, app_bundle_path.clone(), &auth)?;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if matches!(e, NotarizeAuthError::MissingTeamId) {
|
||||
return Err(anyhow::anyhow!("{e}").into());
|
||||
return Err(e.into());
|
||||
} else {
|
||||
log::warn!("skipping app notarization, {}", e.to_string());
|
||||
log::warn!("skipping app notarization, {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -157,8 +172,8 @@ fn copy_binaries_to_bundle(
|
||||
for bin in settings.binaries() {
|
||||
let bin_path = settings.binary_path(bin);
|
||||
let dest_path = dest_dir.join(bin.name());
|
||||
common::copy_file(&bin_path, &dest_path)
|
||||
.with_context(|| format!("Failed to copy binary from {:?}", bin_path))?;
|
||||
fs_utils::copy_file(&bin_path, &dest_path)
|
||||
.with_context(|| format!("Failed to copy binary from {bin_path:?}"))?;
|
||||
paths.push(dest_path);
|
||||
}
|
||||
Ok(paths)
|
||||
@@ -167,17 +182,27 @@ fn copy_binaries_to_bundle(
|
||||
/// Copies user-defined files to the app under Contents.
|
||||
fn copy_custom_files_to_bundle(bundle_directory: &Path, settings: &Settings) -> crate::Result<()> {
|
||||
for (contents_path, path) in settings.macos().files.iter() {
|
||||
if !path.try_exists()? {
|
||||
return Err(GenericError(format!(
|
||||
"Failed to copy {path:?} to {contents_path:?}. {path:?} does not exist."
|
||||
)));
|
||||
}
|
||||
|
||||
let contents_path = if contents_path.is_absolute() {
|
||||
contents_path.strip_prefix("/").unwrap()
|
||||
} else {
|
||||
contents_path
|
||||
};
|
||||
if path.is_file() {
|
||||
common::copy_file(path, bundle_directory.join(contents_path))
|
||||
.with_context(|| format!("Failed to copy file {:?} to {:?}", path, contents_path))?;
|
||||
fs_utils::copy_file(path, &bundle_directory.join(contents_path))
|
||||
.with_context(|| format!("Failed to copy file {path:?} to {contents_path:?}"))?;
|
||||
} else if path.is_dir() {
|
||||
fs_utils::copy_dir(path, &bundle_directory.join(contents_path))
|
||||
.with_context(|| format!("Failed to copy directory {path:?} to {contents_path:?}"))?;
|
||||
} else {
|
||||
common::copy_dir(path, &bundle_directory.join(contents_path))
|
||||
.with_context(|| format!("Failed to copy directory {:?} to {:?}", path, contents_path))?;
|
||||
return Err(GenericError(format!(
|
||||
"{path:?} is not a file or directory."
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -187,14 +212,9 @@ fn copy_custom_files_to_bundle(bundle_directory: &Path, settings: &Settings) ->
|
||||
fn create_info_plist(
|
||||
bundle_dir: &Path,
|
||||
bundle_icon_file: Option<PathBuf>,
|
||||
assets_car_file: Option<PathBuf>,
|
||||
settings: &Settings,
|
||||
) -> crate::Result<()> {
|
||||
let format = time::format_description::parse("[year][month][day].[hour][minute][second]")
|
||||
.map_err(time::error::Error::from)?;
|
||||
let build_number = time::OffsetDateTime::now_utc()
|
||||
.format(&format)
|
||||
.map_err(time::error::Error::from)?;
|
||||
|
||||
let mut plist = plist::Dictionary::new();
|
||||
plist.insert("CFBundleDevelopmentRegion".into(), "English".into());
|
||||
plist.insert("CFBundleDisplayName".into(), settings.product_name().into());
|
||||
@@ -202,29 +222,34 @@ fn create_info_plist(
|
||||
"CFBundleExecutable".into(),
|
||||
settings.main_binary_name()?.into(),
|
||||
);
|
||||
if let Some(path) = bundle_icon_file {
|
||||
plist.insert(
|
||||
"CFBundleIconFile".into(),
|
||||
path
|
||||
.file_name()
|
||||
.expect("No file name")
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
plist.insert(
|
||||
"CFBundleIdentifier".into(),
|
||||
settings.bundle_identifier().into(),
|
||||
);
|
||||
plist.insert("CFBundleInfoDictionaryVersion".into(), "6.0".into());
|
||||
plist.insert("CFBundleName".into(), settings.product_name().into());
|
||||
if let Some(bundle_name) = settings
|
||||
.macos()
|
||||
.bundle_name
|
||||
.as_deref()
|
||||
.unwrap_or_else(|| settings.product_name())
|
||||
.into()
|
||||
{
|
||||
plist.insert("CFBundleName".into(), bundle_name.into());
|
||||
}
|
||||
plist.insert("CFBundlePackageType".into(), "APPL".into());
|
||||
plist.insert(
|
||||
"CFBundleShortVersionString".into(),
|
||||
settings.version_string().into(),
|
||||
);
|
||||
plist.insert("CFBundleVersion".into(), build_number.into());
|
||||
plist.insert(
|
||||
"CFBundleVersion".into(),
|
||||
settings
|
||||
.macos()
|
||||
.bundle_version
|
||||
.as_deref()
|
||||
.unwrap_or_else(|| settings.version_string())
|
||||
.into(),
|
||||
);
|
||||
plist.insert("CSResourcesFileMapped".into(), true.into());
|
||||
if let Some(category) = settings.app_category() {
|
||||
plist.insert(
|
||||
@@ -237,6 +262,55 @@ fn create_info_plist(
|
||||
}
|
||||
|
||||
if let Some(associations) = settings.file_associations() {
|
||||
let exported_associations = associations
|
||||
.iter()
|
||||
.filter_map(|association| {
|
||||
association.exported_type.as_ref().map(|exported_type| {
|
||||
let mut dict = plist::Dictionary::new();
|
||||
|
||||
dict.insert(
|
||||
"UTTypeIdentifier".into(),
|
||||
exported_type.identifier.clone().into(),
|
||||
);
|
||||
if let Some(description) = &association.description {
|
||||
dict.insert("UTTypeDescription".into(), description.clone().into());
|
||||
}
|
||||
if let Some(conforms_to) = &exported_type.conforms_to {
|
||||
dict.insert(
|
||||
"UTTypeConformsTo".into(),
|
||||
plist::Value::Array(conforms_to.iter().map(|s| s.clone().into()).collect()),
|
||||
);
|
||||
}
|
||||
|
||||
let mut specification = plist::Dictionary::new();
|
||||
specification.insert(
|
||||
"public.filename-extension".into(),
|
||||
plist::Value::Array(
|
||||
association
|
||||
.ext
|
||||
.iter()
|
||||
.map(|s| s.to_string().into())
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
if let Some(mime_type) = &association.mime_type {
|
||||
specification.insert("public.mime-type".into(), mime_type.clone().into());
|
||||
}
|
||||
|
||||
dict.insert("UTTypeTagSpecification".into(), specification.into());
|
||||
|
||||
plist::Value::Dictionary(dict)
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !exported_associations.is_empty() {
|
||||
plist.insert(
|
||||
"UTExportedTypeDeclarations".into(),
|
||||
plist::Value::Array(exported_associations),
|
||||
);
|
||||
}
|
||||
|
||||
plist.insert(
|
||||
"CFBundleDocumentTypes".into(),
|
||||
plist::Value::Array(
|
||||
@@ -244,16 +318,27 @@ fn create_info_plist(
|
||||
.iter()
|
||||
.map(|association| {
|
||||
let mut dict = plist::Dictionary::new();
|
||||
dict.insert(
|
||||
"CFBundleTypeExtensions".into(),
|
||||
plist::Value::Array(
|
||||
association
|
||||
.ext
|
||||
.iter()
|
||||
.map(|ext| ext.to_string().into())
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
|
||||
if !association.ext.is_empty() {
|
||||
dict.insert(
|
||||
"CFBundleTypeExtensions".into(),
|
||||
plist::Value::Array(
|
||||
association
|
||||
.ext
|
||||
.iter()
|
||||
.map(|ext| ext.to_string().into())
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(content_types) = &association.content_types {
|
||||
dict.insert(
|
||||
"LSItemContentTypes".into(),
|
||||
plist::Value::Array(content_types.iter().map(|s| s.to_string().into()).collect()),
|
||||
);
|
||||
}
|
||||
|
||||
dict.insert(
|
||||
"CFBundleTypeName".into(),
|
||||
association
|
||||
@@ -267,6 +352,7 @@ fn create_info_plist(
|
||||
"CFBundleTypeRole".into(),
|
||||
association.role.to_string().into(),
|
||||
);
|
||||
dict.insert("LSHandlerRank".into(), association.rank.to_string().into());
|
||||
plist::Value::Dictionary(dict)
|
||||
})
|
||||
.collect(),
|
||||
@@ -274,12 +360,34 @@ fn create_info_plist(
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(path) = bundle_icon_file {
|
||||
plist.insert(
|
||||
"CFBundleIconFile".into(),
|
||||
path
|
||||
.file_name()
|
||||
.expect("No file name")
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(assets_car_file) = assets_car_file {
|
||||
if let Some(icon_name) = app_icon_name_from_assets_car(&assets_car_file) {
|
||||
// only set CFBundleIconName for the Assets.car, CFBundleIconFile is the fallback icns file
|
||||
plist.insert("CFBundleIconName".into(), icon_name.clone().into());
|
||||
} else {
|
||||
log::warn!("Failed to get icon name from Assets.car file");
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(protocols) = settings.deep_link_protocols() {
|
||||
plist.insert(
|
||||
"CFBundleURLTypes".into(),
|
||||
plist::Value::Array(
|
||||
protocols
|
||||
.iter()
|
||||
.filter(|p| !p.schemes.is_empty())
|
||||
.map(|protocol| {
|
||||
let mut dict = plist::Dictionary::new();
|
||||
dict.insert(
|
||||
@@ -330,8 +438,11 @@ fn create_info_plist(
|
||||
plist.insert("NSAppTransportSecurity".into(), security.into());
|
||||
}
|
||||
|
||||
if let Some(user_plist_path) = &settings.macos().info_plist_path {
|
||||
let user_plist = plist::Value::from_file(user_plist_path)?;
|
||||
if let Some(user_plist) = &settings.macos().info_plist {
|
||||
let user_plist = match user_plist {
|
||||
PlistKind::Path(path) => plist::Value::from_file(path)?,
|
||||
PlistKind::Plist(value) => value.clone(),
|
||||
};
|
||||
if let Some(dict) = user_plist.into_dictionary() {
|
||||
for (key, value) in dict {
|
||||
plist.insert(key, value);
|
||||
@@ -346,10 +457,10 @@ fn create_info_plist(
|
||||
|
||||
// Copies the framework under `{src_dir}/{framework}.framework` to `{dest_dir}/{framework}.framework`.
|
||||
fn copy_framework_from(dest_dir: &Path, framework: &str, src_dir: &Path) -> crate::Result<bool> {
|
||||
let src_name = format!("{}.framework", framework);
|
||||
let src_name = format!("{framework}.framework");
|
||||
let src_path = src_dir.join(&src_name);
|
||||
if src_path.exists() {
|
||||
common::copy_dir(&src_path, &dest_dir.join(&src_name))?;
|
||||
fs_utils::copy_dir(&src_path, &dest_dir.join(&src_name))?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
@@ -363,18 +474,12 @@ fn copy_frameworks_to_bundle(
|
||||
) -> crate::Result<Vec<SignTarget>> {
|
||||
let mut paths = Vec::new();
|
||||
|
||||
let frameworks = settings
|
||||
.macos()
|
||||
.frameworks
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let frameworks = settings.macos().frameworks.clone().unwrap_or_default();
|
||||
if frameworks.is_empty() {
|
||||
return Ok(paths);
|
||||
}
|
||||
let dest_dir = bundle_directory.join("Frameworks");
|
||||
fs::create_dir_all(bundle_directory)
|
||||
.with_context(|| format!("Failed to create Frameworks directory at {:?}", dest_dir))?;
|
||||
fs::create_dir_all(&dest_dir).fs_context("failed to create Frameworks directory", &dest_dir)?;
|
||||
for framework in frameworks.iter() {
|
||||
if framework.ends_with(".framework") {
|
||||
let src_path = PathBuf::from(framework);
|
||||
@@ -382,29 +487,25 @@ fn copy_frameworks_to_bundle(
|
||||
.file_name()
|
||||
.expect("Couldn't get framework filename");
|
||||
let dest_path = dest_dir.join(src_name);
|
||||
common::copy_dir(&src_path, &dest_path)?;
|
||||
fs_utils::copy_dir(&src_path, &dest_path)?;
|
||||
add_framework_sign_path(&src_path, &dest_path, &mut paths);
|
||||
continue;
|
||||
} else if framework.ends_with(".dylib") {
|
||||
let src_path = PathBuf::from(framework);
|
||||
if !src_path.exists() {
|
||||
return Err(crate::Error::GenericError(format!(
|
||||
"Library not found: {}",
|
||||
framework
|
||||
)));
|
||||
return Err(GenericError(format!("Library not found: {framework}")));
|
||||
}
|
||||
let src_name = src_path.file_name().expect("Couldn't get library filename");
|
||||
let dest_path = dest_dir.join(src_name);
|
||||
common::copy_file(&src_path, &dest_path)?;
|
||||
fs_utils::copy_file(&src_path, &dest_path)?;
|
||||
paths.push(SignTarget {
|
||||
path: dest_path,
|
||||
is_an_executable: false,
|
||||
});
|
||||
continue;
|
||||
} else if framework.contains('/') {
|
||||
return Err(crate::Error::GenericError(format!(
|
||||
"Framework path should have .framework extension: {}",
|
||||
framework
|
||||
return Err(GenericError(format!(
|
||||
"Framework path should have .framework extension: {framework}"
|
||||
)));
|
||||
}
|
||||
if let Some(home_dir) = dirs::home_dir() {
|
||||
@@ -421,9 +522,8 @@ fn copy_frameworks_to_bundle(
|
||||
{
|
||||
continue;
|
||||
}
|
||||
return Err(crate::Error::GenericError(format!(
|
||||
"Could not locate framework: {}",
|
||||
framework
|
||||
return Err(GenericError(format!(
|
||||
"Could not locate framework: {framework}"
|
||||
)));
|
||||
}
|
||||
Ok(paths)
|
||||
@@ -515,3 +615,153 @@ fn add_nested_code_sign_path(src_path: &Path, dest_path: &Path, sign_paths: &mut
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::bundle::{BundleSettings, MacOsSettings, PackageSettings, SettingsBuilder};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
/// Helper that builds a `Settings` instance and bundle directory for tests.
|
||||
/// It receives a mapping of bundle-relative paths to source paths and
|
||||
/// returns the generated bundle directory and settings.
|
||||
fn create_test_bundle(
|
||||
project_dir: &Path,
|
||||
files: HashMap<PathBuf, PathBuf>,
|
||||
) -> (PathBuf, crate::bundle::Settings) {
|
||||
let macos_settings = MacOsSettings {
|
||||
files,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let settings = SettingsBuilder::new()
|
||||
.project_out_directory(project_dir)
|
||||
.package_settings(PackageSettings {
|
||||
product_name: "TestApp".into(),
|
||||
version: "0.1.0".into(),
|
||||
description: "test".into(),
|
||||
homepage: None,
|
||||
authors: None,
|
||||
default_run: None,
|
||||
})
|
||||
.bundle_settings(BundleSettings {
|
||||
macos: macos_settings,
|
||||
..Default::default()
|
||||
})
|
||||
.target("x86_64-apple-darwin".into())
|
||||
.build()
|
||||
.expect("failed to build settings");
|
||||
|
||||
let bundle_dir = project_dir.join("TestApp.app/Contents");
|
||||
fs::create_dir_all(&bundle_dir).expect("failed to create bundle dir");
|
||||
|
||||
(bundle_dir, settings)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_copy_custom_file_to_bundle_file() {
|
||||
let tmp_dir = tempfile::tempdir().expect("failed to create temp dir");
|
||||
|
||||
// Prepare a single file to copy.
|
||||
let src_file = tmp_dir.path().join("sample.txt");
|
||||
fs::write(&src_file, b"hello tauri").expect("failed to write sample file");
|
||||
|
||||
let files_map = HashMap::from([(PathBuf::from("Resources/sample.txt"), src_file.clone())]);
|
||||
|
||||
let (bundle_dir, settings) = create_test_bundle(tmp_dir.path(), files_map);
|
||||
|
||||
copy_custom_files_to_bundle(&bundle_dir, &settings)
|
||||
.expect("copy_custom_files_to_bundle failed");
|
||||
|
||||
let dest_file = bundle_dir.join("Resources/sample.txt");
|
||||
assert!(dest_file.exists() && dest_file.is_file());
|
||||
assert_eq!(fs::read_to_string(dest_file).unwrap(), "hello tauri");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_copy_custom_file_to_bundle_dir() {
|
||||
let tmp_dir = tempfile::tempdir().expect("failed to create temp dir");
|
||||
|
||||
// Create a source directory with a nested file.
|
||||
let src_dir = tmp_dir.path().join("assets");
|
||||
fs::create_dir_all(&src_dir).expect("failed to create assets directory");
|
||||
let nested_file = src_dir.join("nested.txt");
|
||||
fs::write(&nested_file, b"nested").expect("failed to write nested file");
|
||||
|
||||
let files_map = HashMap::from([(PathBuf::from("MyAssets"), src_dir.clone())]);
|
||||
|
||||
let (bundle_dir, settings) = create_test_bundle(tmp_dir.path(), files_map);
|
||||
|
||||
copy_custom_files_to_bundle(&bundle_dir, &settings)
|
||||
.expect("copy_custom_files_to_bundle failed");
|
||||
|
||||
let dest_nested_file = bundle_dir.join("MyAssets/nested.txt");
|
||||
assert!(
|
||||
dest_nested_file.exists(),
|
||||
"{dest_nested_file:?} does not exist"
|
||||
);
|
||||
assert!(
|
||||
dest_nested_file.is_file(),
|
||||
"{dest_nested_file:?} is not a file"
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read_to_string(dest_nested_file).unwrap().trim(),
|
||||
"nested"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_copy_custom_files_to_bundle_missing_source() {
|
||||
let tmp_dir = tempfile::tempdir().expect("failed to create temp dir");
|
||||
|
||||
// Intentionally reference a non-existent path.
|
||||
let missing_path = tmp_dir.path().join("does_not_exist.txt");
|
||||
|
||||
let files_map = HashMap::from([(PathBuf::from("Missing.txt"), missing_path)]);
|
||||
|
||||
let (bundle_dir, settings) = create_test_bundle(tmp_dir.path(), files_map);
|
||||
|
||||
let result = copy_custom_files_to_bundle(&bundle_dir, &settings);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(result.err().unwrap().to_string().contains("does not exist"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_copy_custom_files_to_bundle_invalid_source() {
|
||||
let tmp_dir = tempfile::tempdir().expect("failed to create temp dir");
|
||||
|
||||
let files_map = HashMap::from([(PathBuf::from("Invalid.txt"), PathBuf::from("///"))]);
|
||||
|
||||
let (bundle_dir, settings) = create_test_bundle(tmp_dir.path(), files_map);
|
||||
|
||||
let result = copy_custom_files_to_bundle(&bundle_dir, &settings);
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.err()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.contains("Failed to copy directory"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_copy_custom_files_to_bundle_dev_null() {
|
||||
let tmp_dir = tempfile::tempdir().expect("failed to create temp dir");
|
||||
|
||||
let files_map = HashMap::from([(PathBuf::from("Invalid.txt"), PathBuf::from("/dev/null"))]);
|
||||
|
||||
let (bundle_dir, settings) = create_test_bundle(tmp_dir.path(), files_map);
|
||||
|
||||
let result = copy_custom_files_to_bundle(&bundle_dir, &settings);
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.err()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.contains("is not a file or directory."));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,70 +4,22 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
# Create a read-only disk image of the contents of a folder
|
||||
# forked from https://github.com/andreyvit/create-dmg
|
||||
# forked from https://github.com/create-dmg/create-dmg
|
||||
|
||||
# Bail out on any unhandled errors
|
||||
set -ex;
|
||||
set -e;
|
||||
# Any command that exits with non-zero code will cause the pipeline to fail
|
||||
set -o pipefail;
|
||||
|
||||
function pure_version() {
|
||||
echo '1.0.0.6'
|
||||
}
|
||||
CDMG_VERSION='1.2.1'
|
||||
|
||||
function version() {
|
||||
echo "create-dmg $(pure_version)"
|
||||
}
|
||||
|
||||
function usage() {
|
||||
version
|
||||
echo "Creates a fancy DMG file."
|
||||
echo "Usage: $(basename "$0") [options] <output_name.dmg> <source_folder>"
|
||||
echo "All contents of <source_folder> will be copied into the disk image."
|
||||
echo "Options:"
|
||||
echo " --volname name"
|
||||
echo " set volume name (displayed in the Finder sidebar and window title)"
|
||||
echo " --volicon icon.icns"
|
||||
echo " set volume icon"
|
||||
echo " --background pic.png"
|
||||
echo " set folder background image (provide png, gif, jpg)"
|
||||
echo " --window-pos x y"
|
||||
echo " set position the folder window"
|
||||
echo " --window-size width height"
|
||||
echo " set size of the folder window"
|
||||
echo " --text-size text_size"
|
||||
echo " set window text size (10-16)"
|
||||
echo " --icon-size icon_size"
|
||||
echo " set window icons size (up to 128)"
|
||||
echo " --icon file_name x y"
|
||||
echo " set position of the file's icon"
|
||||
echo " --hide-extension file_name"
|
||||
echo " hide the extension of file"
|
||||
echo " --app-drop-link x y"
|
||||
echo " make a drop link to Applications, at location x,y"
|
||||
echo " --ql-drop-link x y"
|
||||
echo " make a drop link to user QuickLook install dir, at location x,y"
|
||||
echo " --eula eula_file"
|
||||
echo " attach a license file to the dmg"
|
||||
echo " --no-internet-enable"
|
||||
echo " disable automatic mount©"
|
||||
echo " --format"
|
||||
echo " specify the final image format (default is UDZO)"
|
||||
echo " --add-file target_name file|folder x y"
|
||||
echo " add additional file or folder (can be used multiple times)"
|
||||
echo " --disk-image-size x"
|
||||
echo " set the disk image size manually to x MB"
|
||||
echo " --hdiutil-verbose"
|
||||
echo " execute hdiutil in verbose mode"
|
||||
echo " --hdiutil-quiet"
|
||||
echo " execute hdiutil in quiet mode"
|
||||
echo " --bless"
|
||||
echo " bless the mount folder (deprecated, needs macOS 12.2.1 or older)"
|
||||
echo " --sandbox-safe"
|
||||
echo " execute hdiutil with sandbox compatibility, do not bless and do not execute the cosmetic AppleScript"
|
||||
echo " --version show tool version number"
|
||||
echo " -h, --help display this help"
|
||||
exit 0
|
||||
}
|
||||
# The full path to the "support/" directory this script is using
|
||||
# (This will be set up by code later in the script.)
|
||||
CDMG_SUPPORT_DIR=""
|
||||
|
||||
OS_FULL_VERSION="$(sw_vers | sed -n 2p | cut -d : -f 2 | tr -d '[:space:]' | cut -c1-)"
|
||||
OS_MAJOR_VERSION="$(echo $OS_FULL_VERSION | cut -d . -f 1)"
|
||||
OS_MINOR_VERSION="$(echo $OS_FULL_VERSION | cut -d . -f 2)"
|
||||
WINX=10
|
||||
WINY=60
|
||||
WINW=500
|
||||
@@ -75,6 +27,7 @@ WINH=350
|
||||
ICON_SIZE=128
|
||||
TEXT_SIZE=16
|
||||
FORMAT="UDZO"
|
||||
FILESYSTEM="HFS+"
|
||||
ADD_FILE_SOURCES=()
|
||||
ADD_FILE_TARGETS=()
|
||||
IMAGEKEY=""
|
||||
@@ -83,116 +36,259 @@ SANDBOX_SAFE=0
|
||||
BLESS=0
|
||||
SKIP_JENKINS=0
|
||||
MAXIMUM_UNMOUNTING_ATTEMPTS=3
|
||||
POSITION_CLAUSE=""
|
||||
HIDING_CLAUSE=""
|
||||
SIGNATURE=""
|
||||
NOTARIZE=""
|
||||
|
||||
function pure_version() {
|
||||
echo "$CDMG_VERSION"
|
||||
}
|
||||
|
||||
function hdiutil_detach_retry() {
|
||||
# Unmount
|
||||
unmounting_attempts=0
|
||||
until
|
||||
echo "Unmounting disk image..."
|
||||
(( unmounting_attempts++ ))
|
||||
hdiutil detach "$1"
|
||||
exit_code=$?
|
||||
(( exit_code == 0 )) && break # nothing goes wrong
|
||||
(( exit_code != 16 )) && exit $exit_code # exit with the original exit code
|
||||
# The above statement returns 1 if test failed (exit_code == 16).
|
||||
# It can make the code in the {do... done} block to be executed
|
||||
do
|
||||
(( unmounting_attempts == MAXIMUM_UNMOUNTING_ATTEMPTS )) && exit 16 # patience exhausted, exit with code EBUSY
|
||||
echo "Wait a moment..."
|
||||
sleep $(( 1 * (2 ** unmounting_attempts) ))
|
||||
done
|
||||
unset unmounting_attempts
|
||||
}
|
||||
|
||||
function version() {
|
||||
echo "create-dmg $(pure_version)"
|
||||
}
|
||||
|
||||
function usage() {
|
||||
version
|
||||
cat <<EOHELP
|
||||
|
||||
Creates a fancy DMG file.
|
||||
|
||||
Usage: $(basename $0) [options] <output_name.dmg> <source_folder>
|
||||
|
||||
All contents of <source_folder> will be copied into the disk image.
|
||||
|
||||
Options:
|
||||
--volname <name>
|
||||
set volume name (displayed in the Finder sidebar and window title)
|
||||
--volicon <icon.icns>
|
||||
set volume icon
|
||||
--background <pic.png>
|
||||
set folder background image (provide png, gif, or jpg)
|
||||
--window-pos <x> <y>
|
||||
set position the folder window
|
||||
--window-size <width> <height>
|
||||
set size of the folder window
|
||||
--text-size <text_size>
|
||||
set window text size (10-16)
|
||||
--icon-size <icon_size>
|
||||
set window icons size (up to 128)
|
||||
--icon file_name <x> <y>
|
||||
set position of the file's icon
|
||||
--hide-extension <file_name>
|
||||
hide the extension of file
|
||||
--app-drop-link <x> <y>
|
||||
make a drop link to Applications, at location x,y
|
||||
--ql-drop-link <x> <y>
|
||||
make a drop link to user QuickLook install dir, at location x,y
|
||||
--eula <eula_file>
|
||||
attach a license file to the dmg (plain text or RTF)
|
||||
--no-internet-enable
|
||||
disable automatic mount & copy
|
||||
--format <format>
|
||||
specify the final disk image format (UDZO|UDBZ|ULFO|ULMO) (default is UDZO)
|
||||
--filesystem <filesystem>
|
||||
specify the disk image filesystem (HFS+|APFS) (default is HFS+, APFS supports macOS 10.13 or newer)
|
||||
--encrypt
|
||||
enable encryption for the resulting disk image (AES-256 - you will be prompted for password)
|
||||
--encrypt-aes128
|
||||
enable encryption for the resulting disk image (AES-128 - you will be prompted for password)
|
||||
--add-file <target_name> <file>|<folder> <x> <y>
|
||||
add additional file or folder (can be used multiple times)
|
||||
--disk-image-size <x>
|
||||
set the disk image size manually to x MB
|
||||
--hdiutil-verbose
|
||||
execute hdiutil in verbose mode
|
||||
--hdiutil-quiet
|
||||
execute hdiutil in quiet mode
|
||||
--bless
|
||||
bless the mount folder (deprecated, needs macOS 12.2.1 or older)
|
||||
--codesign <signature>
|
||||
codesign the disk image with the specified signature
|
||||
--notarize <credentials>
|
||||
notarize the disk image (waits and staples) with the keychain stored credentials
|
||||
--sandbox-safe
|
||||
execute hdiutil with sandbox compatibility and do not bless (not supported for APFS disk images)
|
||||
--skip-jenkins
|
||||
skip Finder-prettifying AppleScript, useful in Sandbox and non-GUI environments
|
||||
--version
|
||||
show create-dmg version number
|
||||
-h, --help
|
||||
display this help screen
|
||||
|
||||
EOHELP
|
||||
exit 0
|
||||
}
|
||||
|
||||
# factors can cause interstitial disk images to contain more than a single
|
||||
# partition - expand the hunt for the temporary disk image by checking for
|
||||
# the path of the volume, versus assuming its the first result (as in pr/152).
|
||||
function find_mount_dir() {
|
||||
local dev_name="${1}"
|
||||
|
||||
>&2 echo "Searching for mounted interstitial disk image using ${dev_name}... "
|
||||
# enumerate up to 9 partitions
|
||||
for i in {1..9}; do
|
||||
# attempt to find the partition
|
||||
local found_dir
|
||||
found_dir=$(hdiutil info | grep -E --color=never "${dev_name}" | head -${i} | awk '{print $3}' | xargs)
|
||||
if [[ -n "${found_dir}" ]]; then
|
||||
echo "${found_dir}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Argument parsing
|
||||
|
||||
while [[ "${1:0:1}" = "-" ]]; do
|
||||
case $1 in
|
||||
--volname)
|
||||
VOLUME_NAME="$2"
|
||||
shift; shift;;
|
||||
--volicon)
|
||||
VOLUME_ICON_FILE="$2"
|
||||
shift; shift;;
|
||||
--background)
|
||||
BACKGROUND_FILE="$2"
|
||||
BACKGROUND_FILE_NAME="$(basename "$BACKGROUND_FILE")"
|
||||
BACKGROUND_CLAUSE="set background picture of opts to file \".background:$BACKGROUND_FILE_NAME\""
|
||||
REPOSITION_HIDDEN_FILES_CLAUSE="set position of every item to {theBottomRightX + 100, 100}"
|
||||
shift; shift;;
|
||||
--icon-size)
|
||||
ICON_SIZE="$2"
|
||||
shift; shift;;
|
||||
--text-size)
|
||||
TEXT_SIZE="$2"
|
||||
shift; shift;;
|
||||
--window-pos)
|
||||
WINX=$2; WINY=$3
|
||||
shift; shift; shift;;
|
||||
--window-size)
|
||||
WINW=$2; WINH=$3
|
||||
shift; shift; shift;;
|
||||
--icon)
|
||||
POSITION_CLAUSE="${POSITION_CLAUSE}set position of item \"$2\" to {$3, $4}
|
||||
"
|
||||
shift; shift; shift; shift;;
|
||||
--hide-extension)
|
||||
HIDING_CLAUSE="${HIDING_CLAUSE}set the extension hidden of item \"$2\" to true
|
||||
"
|
||||
shift; shift;;
|
||||
-h | --help)
|
||||
usage;;
|
||||
--version)
|
||||
version; exit 0;;
|
||||
--pure-version)
|
||||
pure_version; exit 0;;
|
||||
--ql-drop-link)
|
||||
QL_LINK=$2
|
||||
QL_CLAUSE="set position of item \"QuickLook\" to {$2, $3}
|
||||
"
|
||||
shift; shift; shift;;
|
||||
--app-drop-link)
|
||||
APPLICATION_LINK=$2
|
||||
APPLICATION_CLAUSE="set position of item \"Applications\" to {$2, $3}
|
||||
"
|
||||
shift; shift; shift;;
|
||||
--eula)
|
||||
EULA_RSRC=$2
|
||||
shift; shift;;
|
||||
--no-internet-enable)
|
||||
NOINTERNET=1
|
||||
shift;;
|
||||
--format)
|
||||
FORMAT="$2"
|
||||
shift; shift;;
|
||||
--add-file | --add-folder)
|
||||
ADD_FILE_TARGETS+=("$2")
|
||||
ADD_FILE_SOURCES+=("$3")
|
||||
POSITION_CLAUSE="${POSITION_CLAUSE}
|
||||
set position of item \"$2\" to {$4, $5}
|
||||
"
|
||||
shift; shift; shift; shift; shift;;
|
||||
--disk-image-size)
|
||||
DISK_IMAGE_SIZE="$2"
|
||||
shift; shift;;
|
||||
--hdiutil-verbose)
|
||||
HDIUTIL_VERBOSITY='-verbose'
|
||||
shift;;
|
||||
--hdiutil-quiet)
|
||||
HDIUTIL_VERBOSITY='-quiet'
|
||||
shift;;
|
||||
--sandbox-safe)
|
||||
SANDBOX_SAFE=1
|
||||
shift;;
|
||||
--bless)
|
||||
BLESS=1
|
||||
shift;;
|
||||
--skip-jenkins)
|
||||
SKIP_JENKINS=1
|
||||
shift;;
|
||||
-*)
|
||||
echo "Unknown option: $1. Run with --help for help."
|
||||
exit 1;;
|
||||
--volname)
|
||||
VOLUME_NAME="$2"
|
||||
shift; shift;;
|
||||
--volicon)
|
||||
VOLUME_ICON_FILE="$2"
|
||||
shift; shift;;
|
||||
--background)
|
||||
BACKGROUND_FILE="$2"
|
||||
BACKGROUND_FILE_NAME="$(basename "$BACKGROUND_FILE")"
|
||||
BACKGROUND_CLAUSE="set background picture of opts to file \".background:$BACKGROUND_FILE_NAME\""
|
||||
REPOSITION_HIDDEN_FILES_CLAUSE="set position of every item to {theBottomRightX + 100, 100}"
|
||||
shift; shift;;
|
||||
--icon-size)
|
||||
ICON_SIZE="$2"
|
||||
shift; shift;;
|
||||
--text-size)
|
||||
TEXT_SIZE="$2"
|
||||
shift; shift;;
|
||||
--window-pos)
|
||||
WINX=$2; WINY=$3
|
||||
shift; shift; shift;;
|
||||
--window-size)
|
||||
WINW=$2; WINH=$3
|
||||
shift; shift; shift;;
|
||||
--icon)
|
||||
POSITION_CLAUSE="${POSITION_CLAUSE}set position of item \"$2\" to {$3, $4}
|
||||
"
|
||||
shift; shift; shift; shift;;
|
||||
--hide-extension)
|
||||
HIDING_CLAUSE="${HIDING_CLAUSE}set the extension hidden of item \"$2\" to true
|
||||
"
|
||||
shift; shift;;
|
||||
-h | --help)
|
||||
usage;;
|
||||
--version)
|
||||
version; exit 0;;
|
||||
--pure-version)
|
||||
pure_version; exit 0;;
|
||||
--ql-drop-link)
|
||||
QL_LINK=$2
|
||||
QL_CLAUSE="set position of item \"QuickLook\" to {$2, $3}
|
||||
"
|
||||
shift; shift; shift;;
|
||||
--app-drop-link)
|
||||
APPLICATION_LINK=$2
|
||||
APPLICATION_CLAUSE="set position of item \"Applications\" to {$2, $3}
|
||||
"
|
||||
shift; shift; shift;;
|
||||
--eula)
|
||||
EULA_RSRC=$2
|
||||
shift; shift;;
|
||||
--no-internet-enable)
|
||||
NOINTERNET=1
|
||||
shift;;
|
||||
--format)
|
||||
FORMAT="$2"
|
||||
shift; shift;;
|
||||
--filesystem)
|
||||
FILESYSTEM="$2"
|
||||
shift; shift;;
|
||||
--encrypt)
|
||||
ENABLE_ENCRYPTION=1
|
||||
AESBITS=256
|
||||
shift;;
|
||||
--encrypt-aes128)
|
||||
ENABLE_ENCRYPTION=1
|
||||
AESBITS=128
|
||||
shift;;
|
||||
--add-file | --add-folder)
|
||||
ADD_FILE_TARGETS+=("$2")
|
||||
ADD_FILE_SOURCES+=("$3")
|
||||
POSITION_CLAUSE="${POSITION_CLAUSE}
|
||||
set position of item \"$2\" to {$4, $5}
|
||||
"
|
||||
shift; shift; shift; shift; shift;;
|
||||
--disk-image-size)
|
||||
DISK_IMAGE_SIZE="$2"
|
||||
shift; shift;;
|
||||
--hdiutil-verbose)
|
||||
HDIUTIL_VERBOSITY='-verbose'
|
||||
shift;;
|
||||
--hdiutil-quiet)
|
||||
HDIUTIL_VERBOSITY='-quiet'
|
||||
shift;;
|
||||
--codesign)
|
||||
SIGNATURE="$2"
|
||||
shift; shift;;
|
||||
--notarize)
|
||||
NOTARIZE="$2"
|
||||
shift; shift;;
|
||||
--sandbox-safe)
|
||||
SANDBOX_SAFE=1
|
||||
shift;;
|
||||
--bless)
|
||||
BLESS=1
|
||||
shift;;
|
||||
--rez)
|
||||
echo "REZ is no more directly used. You can remove the --rez argument."
|
||||
shift; shift;;
|
||||
--skip-jenkins)
|
||||
SKIP_JENKINS=1
|
||||
shift;;
|
||||
-*)
|
||||
echo "Unknown option: $1. Run 'create-dmg --help' for help."
|
||||
exit 1;;
|
||||
esac
|
||||
case $FORMAT in
|
||||
UDZO)
|
||||
IMAGEKEY="-imagekey zlib-level=9";;
|
||||
UDBZ)
|
||||
IMAGEKEY="-imagekey bzip2-level=9";;
|
||||
UDZO)
|
||||
IMAGEKEY="-imagekey zlib-level=9";;
|
||||
UDBZ)
|
||||
IMAGEKEY="-imagekey bzip2-level=9";;
|
||||
ULFO)
|
||||
;;
|
||||
ULMO)
|
||||
;;
|
||||
*)
|
||||
echo >&2 "Unknown disk image format: $FORMAT"
|
||||
exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$2" ]]; then
|
||||
echo "Not enough arguments. Invoke with --help for help."
|
||||
echo "Not enough arguments. Run 'create-dmg --help' for help."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
DMG_PATH="$1"
|
||||
DMG_DIRNAME="$(dirname "$DMG_PATH")"
|
||||
DMG_DIR="$(cd "$DMG_DIRNAME" > /dev/null; pwd)"
|
||||
DMG_NAME="$(basename "$DMG_PATH")"
|
||||
DMG_TEMP_NAME="$DMG_DIR/rw.${DMG_NAME}"
|
||||
SRC_FOLDER="$(cd "$2" > /dev/null; pwd)"
|
||||
|
||||
# Argument validation checks
|
||||
@@ -202,24 +298,48 @@ if [[ "${DMG_PATH: -4}" != ".dmg" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${FILESYSTEM}" != "HFS+" ]] && [[ "${FILESYSTEM}" != "APFS" ]]; then
|
||||
echo "Unknown disk image filesystem: ${FILESYSTEM}. Run 'create-dmg --help' for help."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${FILESYSTEM}" == "APFS" ]] && [[ ${SANDBOX_SAFE} -eq 1 ]]; then
|
||||
echo "Creating an APFS disk image that is sandbox safe is not supported."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Main script logic
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
DMG_DIRNAME="$(dirname "$DMG_PATH")"
|
||||
DMG_DIR="$(cd "$DMG_DIRNAME" > /dev/null; pwd)"
|
||||
DMG_NAME="$(basename "$DMG_PATH")"
|
||||
DMG_TEMP_NAME="$DMG_DIR/rw.$$.${DMG_NAME}"
|
||||
|
||||
# Detect where we're running from
|
||||
|
||||
sentinel_file="$SCRIPT_DIR/.this-is-the-create-dmg-repo"
|
||||
if [[ -f "$sentinel_file" ]]; then
|
||||
# We're running from inside a repo
|
||||
CDMG_SUPPORT_DIR="$SCRIPT_DIR/support"
|
||||
else
|
||||
# We're running inside an installed location
|
||||
bin_dir="$SCRIPT_DIR"
|
||||
prefix_dir=$(dirname "$bin_dir")
|
||||
CDMG_SUPPORT_DIR="$prefix_dir/share/create-dmg/support"
|
||||
fi
|
||||
|
||||
if [[ -z "$VOLUME_NAME" ]]; then
|
||||
VOLUME_NAME="$(basename "$DMG_PATH" .dmg)"
|
||||
fi
|
||||
|
||||
# brew formula will set this as 1 and embed the support scripts
|
||||
BREW_INSTALL=0
|
||||
|
||||
AUX_PATH="$SCRIPT_DIR/support"
|
||||
|
||||
if [ $BREW_INSTALL -eq 0 ]; then
|
||||
test -d "$AUX_PATH" || {
|
||||
echo "Cannot find support directory: $AUX_PATH"
|
||||
exit 1
|
||||
}
|
||||
if [[ ! -d "$CDMG_SUPPORT_DIR" ]]; then
|
||||
echo >&2 "Cannot find support/ directory: expected at: $CDMG_SUPPORT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -f "$SRC_FOLDER/.DS_Store" ]]; then
|
||||
echo "Deleting any .DS_Store in source folder"
|
||||
echo "Deleting .DS_Store found in source folder"
|
||||
rm "$SRC_FOLDER/.DS_Store"
|
||||
fi
|
||||
|
||||
@@ -229,7 +349,7 @@ if [[ -f "${DMG_TEMP_NAME}" ]]; then
|
||||
rm -f "${DMG_TEMP_NAME}"
|
||||
fi
|
||||
|
||||
# Using Megabytes since hdiutil fails with very large Byte numbers
|
||||
# Use Megabytes since hdiutil fails with very large byte numbers
|
||||
function blocks_to_megabytes() {
|
||||
# Add 1 extra MB, since there's no decimal retention here
|
||||
MB_SIZE=$((($1 * 512 / 1000 / 1000) + 1))
|
||||
@@ -238,8 +358,13 @@ function blocks_to_megabytes() {
|
||||
|
||||
function get_size() {
|
||||
# Get block size in disk
|
||||
bytes_size=$(du -s "$1" | sed -e 's/ .*//g')
|
||||
echo $(blocks_to_megabytes "$bytes_size")
|
||||
if [[ $OS_MAJOR_VERSION -ge 12 ]]; then
|
||||
bytes_size=$(du -B 512 -s "$1")
|
||||
else
|
||||
bytes_size=$(du -s "$1")
|
||||
fi
|
||||
bytes_size=$(echo $bytes_size | sed -e 's/ .*//g')
|
||||
echo $(blocks_to_megabytes $bytes_size)
|
||||
}
|
||||
|
||||
# Create the DMG with the specified size or the hdiutil estimation
|
||||
@@ -248,8 +373,14 @@ if [[ -n "$DISK_IMAGE_SIZE" ]]; then
|
||||
CUSTOM_SIZE="-size ${DISK_IMAGE_SIZE}m"
|
||||
fi
|
||||
|
||||
if [ $SANDBOX_SAFE -eq 0 ]; then
|
||||
hdiutil create ${HDIUTIL_VERBOSITY} -srcfolder "$SRC_FOLDER" -volname "${VOLUME_NAME}" -fs HFS+ -fsargs "-c c=64,a=16,e=16" -format UDRW ${CUSTOM_SIZE} "${DMG_TEMP_NAME}"
|
||||
if [[ $SANDBOX_SAFE -eq 0 ]]; then
|
||||
if [[ "$FILESYSTEM" == "APFS" ]]; then
|
||||
FILESYSTEM_ARGUMENTS=""
|
||||
else
|
||||
FILESYSTEM_ARGUMENTS="-c c=64,a=16,e=16"
|
||||
fi
|
||||
hdiutil create ${HDIUTIL_VERBOSITY} -srcfolder "$SRC_FOLDER" -volname "${VOLUME_NAME}" \
|
||||
-fs "${FILESYSTEM}" -fsargs "${FILESYSTEM_ARGUMENTS}" -format UDRW ${CUSTOM_SIZE} "${DMG_TEMP_NAME}"
|
||||
else
|
||||
hdiutil makehybrid ${HDIUTIL_VERBOSITY} -default-volume-name "${VOLUME_NAME}" -hfs -o "${DMG_TEMP_NAME}" "$SRC_FOLDER"
|
||||
hdiutil convert -format UDRW -ov -o "${DMG_TEMP_NAME}" "${DMG_TEMP_NAME}"
|
||||
@@ -260,7 +391,7 @@ fi
|
||||
DISK_IMAGE_SIZE=$(get_size "${DMG_TEMP_NAME}")
|
||||
|
||||
# Use the custom size if bigger
|
||||
if [[ $SANDBOX_SAFE -eq 1 ]] && [[ -n "$DISK_IMAGE_SIZE_CUSTOM" ]] && [[ $DISK_IMAGE_SIZE_CUSTOM -gt $DISK_IMAGE_SIZE ]]; then
|
||||
if [[ $SANDBOX_SAFE -eq 1 ]] && [[ ! -z "$DISK_IMAGE_SIZE_CUSTOM" ]] && [[ $DISK_IMAGE_SIZE_CUSTOM -gt $DISK_IMAGE_SIZE ]]; then
|
||||
DISK_IMAGE_SIZE=$DISK_IMAGE_SIZE_CUSTOM
|
||||
fi
|
||||
|
||||
@@ -278,28 +409,39 @@ DISK_IMAGE_SIZE=$(expr $DISK_IMAGE_SIZE + 20)
|
||||
# Make sure target image size is within limits
|
||||
MIN_DISK_IMAGE_SIZE=$(hdiutil resize -limits "${DMG_TEMP_NAME}" | awk 'NR=1{print int($1/2048+1)}')
|
||||
if [ $MIN_DISK_IMAGE_SIZE -gt $DISK_IMAGE_SIZE ]; then
|
||||
DISK_IMAGE_SIZE=$MIN_DISK_IMAGE_SIZE
|
||||
DISK_IMAGE_SIZE=$MIN_DISK_IMAGE_SIZE
|
||||
fi
|
||||
|
||||
# Resize the image for the extra stuff
|
||||
hdiutil resize ${HDIUTIL_VERBOSITY} -size ${DISK_IMAGE_SIZE}m "${DMG_TEMP_NAME}"
|
||||
|
||||
# mount the new DMG
|
||||
echo "Mounting disk image..."
|
||||
MOUNT_DIR="/Volumes/${VOLUME_NAME}"
|
||||
# Mount the new DMG
|
||||
|
||||
# Unmount leftover dmg if it was mounted previously (e.g. developer mounted dmg, installed app and forgot to unmount it)
|
||||
if [[ -d "${MOUNT_DIR}" ]]; then
|
||||
echo "Unmounting previously mounted disk image..."
|
||||
DEV_NAME=$(hdiutil info | grep -E --color=never '^/dev/' | sed 1q | awk '{print $1}')
|
||||
hdiutil detach "${DEV_NAME}"
|
||||
echo "Mounting disk image..."
|
||||
|
||||
MOUNT_RANDOM_PATH="/Volumes"
|
||||
if [[ $SANDBOX_SAFE -eq 1 ]]; then
|
||||
MOUNT_RANDOM_PATH="/tmp"
|
||||
fi
|
||||
if [[ "$FILESYSTEM" == "APFS" ]]; then
|
||||
HDIUTIL_FILTER="tail -n 1"
|
||||
else
|
||||
HDIUTIL_FILTER="sed 1q"
|
||||
fi
|
||||
DEV_NAME=$(hdiutil attach -mountrandom ${MOUNT_RANDOM_PATH} -readwrite -noverify -noautoopen -nobrowse "${DMG_TEMP_NAME}" | grep -E --color=never '^/dev/' | ${HDIUTIL_FILTER} | awk '{print $1}')
|
||||
echo "Device name: $DEV_NAME"
|
||||
if [[ "$FILESYSTEM" == "APFS" ]]; then
|
||||
MOUNT_DIR=$(find_mount_dir "${DEV_NAME}")
|
||||
else
|
||||
MOUNT_DIR=$(find_mount_dir "${DEV_NAME}s")
|
||||
fi
|
||||
if [[ -z "${MOUNT_DIR}" ]]; then
|
||||
>&2 echo "ERROR: unable to proceed with final disk image creation because the interstitial disk image was not found."
|
||||
>&2 echo "The interstitial disk image will likely be mounted and will need to be cleaned up manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Mounting disk image..."
|
||||
|
||||
echo "Mount directory: $MOUNT_DIR"
|
||||
DEV_NAME=$(hdiutil attach -readwrite -noverify -noautoopen "${DMG_TEMP_NAME}" | grep -E --color=never '^/dev/' | sed 1q | awk '{print $1}')
|
||||
echo "Device name: $DEV_NAME"
|
||||
echo "Mount dir: $MOUNT_DIR"
|
||||
|
||||
if [[ -n "$BACKGROUND_FILE" ]]; then
|
||||
echo "Copying background file '$BACKGROUND_FILE'..."
|
||||
@@ -308,12 +450,14 @@ if [[ -n "$BACKGROUND_FILE" ]]; then
|
||||
fi
|
||||
|
||||
if [[ -n "$APPLICATION_LINK" ]]; then
|
||||
echo "making link to Applications dir"
|
||||
test -d "$MOUNT_DIR/Applications" || ln -s /Applications "$MOUNT_DIR/Applications"
|
||||
echo "Making link to Applications dir..."
|
||||
echo $MOUNT_DIR
|
||||
ln -s /Applications "$MOUNT_DIR/Applications"
|
||||
fi
|
||||
|
||||
if [[ -n "$QL_LINK" ]]; then
|
||||
echo "making link to QuickLook install dir"
|
||||
echo "Making link to QuickLook install dir..."
|
||||
echo $MOUNT_DIR
|
||||
ln -s "/Library/QuickLook" "$MOUNT_DIR/QuickLook"
|
||||
fi
|
||||
|
||||
@@ -331,24 +475,15 @@ if [[ -n "$ADD_FILE_SOURCES" ]]; then
|
||||
done
|
||||
fi
|
||||
|
||||
# run AppleScript to do all the Finder cosmetic stuff
|
||||
VOLUME_NAME=$(basename $MOUNT_DIR)
|
||||
|
||||
# Run AppleScript to do all the Finder cosmetic stuff
|
||||
APPLESCRIPT_FILE=$(mktemp -t createdmg.tmp.XXXXXXXXXX)
|
||||
|
||||
function applescript_source() {
|
||||
if [ $BREW_INSTALL -eq 0 ]; then
|
||||
cat "$AUX_PATH/template.applescript"
|
||||
else
|
||||
cat << 'EOS'
|
||||
# BREW_INLINE_APPLESCRIPT_PLACEHOLDER
|
||||
EOS
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ $SANDBOX_SAFE -eq 1 ]]; then
|
||||
echo "Skipping Finder-prettifying AppleScript because we are in Sandbox..."
|
||||
else
|
||||
if [[ $SKIP_JENKINS -eq 0 ]]; then
|
||||
applescript_source \
|
||||
cat "$CDMG_SUPPORT_DIR/template.applescript" \
|
||||
| sed -e "s/WINX/$WINX/g" -e "s/WINY/$WINY/g" -e "s/WINW/$WINW/g" \
|
||||
-e "s/WINH/$WINH/g" -e "s/BACKGROUND_CLAUSE/$BACKGROUND_CLAUSE/g" \
|
||||
-e "s/REPOSITION_HIDDEN_FILES_CLAUSE/$REPOSITION_HIDDEN_FILES_CLAUSE/g" \
|
||||
@@ -358,71 +493,76 @@ else
|
||||
| perl -pe "s/APPLICATION_CLAUSE/$APPLICATION_CLAUSE/g" \
|
||||
| perl -pe "s/HIDING_CLAUSE/$HIDING_CLAUSE/" \
|
||||
> "$APPLESCRIPT_FILE"
|
||||
sleep 2 # pause to workaround occasional "Can't get disk" (-1728) issues
|
||||
|
||||
# pause to workaround occasional "Can’t get disk" (-1728) issues
|
||||
ERROR_1728_WORKAROUND_SLEEP_INTERVAL=2
|
||||
echo "Will sleep for $ERROR_1728_WORKAROUND_SLEEP_INTERVAL seconds to workaround occasions \"Can't get disk (-1728)\" issues..."
|
||||
sleep $ERROR_1728_WORKAROUND_SLEEP_INTERVAL
|
||||
|
||||
echo "Running AppleScript to make Finder stuff pretty: /usr/bin/osascript \"${APPLESCRIPT_FILE}\" \"${VOLUME_NAME}\""
|
||||
if /usr/bin/osascript "${APPLESCRIPT_FILE}" "${VOLUME_NAME}"; then
|
||||
# Okay, we're cool
|
||||
true
|
||||
else
|
||||
echo >&2 "Failed running AppleScript"
|
||||
hdiutil detach "${DEV_NAME}"
|
||||
hdiutil_detach_retry "${DEV_NAME}"
|
||||
exit 64
|
||||
fi
|
||||
echo "Done running the AppleScript..."
|
||||
sleep 4
|
||||
rm "$APPLESCRIPT_FILE"
|
||||
else
|
||||
echo ''
|
||||
echo "Will skip running AppleScript to configure DMG aesthetics because of --skip-jenkins option."
|
||||
echo "This will result in a DMG without any custom background or icons positioning."
|
||||
echo "More info at https://github.com/create-dmg/create-dmg/issues/72"
|
||||
echo ''
|
||||
fi
|
||||
fi
|
||||
|
||||
# make sure it's not world writeable
|
||||
# Make sure it's not world writeable
|
||||
echo "Fixing permissions..."
|
||||
chmod -Rf go-w "${MOUNT_DIR}" &> /dev/null || true
|
||||
echo "Done fixing permissions."
|
||||
echo "Done fixing permissions"
|
||||
|
||||
# make the top window open itself on mount:
|
||||
# Make the top window open itself on mount:
|
||||
if [[ $BLESS -eq 1 && $SANDBOX_SAFE -eq 0 ]]; then
|
||||
echo "Blessing started"
|
||||
bless --folder "${MOUNT_DIR}" --openfolder "${MOUNT_DIR}"
|
||||
if [ $(uname -m) == "arm64" ]; then
|
||||
bless --folder "${MOUNT_DIR}"
|
||||
else
|
||||
bless --folder "${MOUNT_DIR}" --openfolder "${MOUNT_DIR}"
|
||||
fi
|
||||
echo "Blessing finished"
|
||||
else
|
||||
echo "Skipping blessing on sandbox"
|
||||
fi
|
||||
|
||||
if [[ -n "$VOLUME_ICON_FILE" ]]; then
|
||||
# tell the volume that it has a special file attribute
|
||||
# Tell the volume that it has a special file attribute
|
||||
SetFile -a C "$MOUNT_DIR"
|
||||
fi
|
||||
|
||||
# Delete unnecessary file system events log
|
||||
# Delete unnecessary file system events log if possible
|
||||
echo "Deleting .fseventsd"
|
||||
rm -rf "${MOUNT_DIR}/.fseventsd"
|
||||
rm -rf "${MOUNT_DIR}/.fseventsd" || true
|
||||
|
||||
# unmount
|
||||
unmounting_attempts=0
|
||||
until
|
||||
echo "Unmounting disk image..."
|
||||
(( unmounting_attempts++ ))
|
||||
hdiutil detach "${DEV_NAME}"
|
||||
exit_code=$?
|
||||
(( exit_code == 0 )) && break # nothing goes wrong
|
||||
(( exit_code != 16 )) && exit $exit_code # exit with the original exit code
|
||||
# The above statement returns 1 if test failed (exit_code == 16).
|
||||
# It can make the code in the {do... done} block to be executed
|
||||
do
|
||||
(( unmounting_attempts == MAXIMUM_UNMOUNTING_ATTEMPTS )) && exit 16 # patience exhausted, exit with code EBUSY
|
||||
echo "Wait a moment..."
|
||||
sleep $(( 1 * (2 ** unmounting_attempts) ))
|
||||
done
|
||||
unset unmounting_attempts
|
||||
hdiutil_detach_retry "${DEV_NAME}"
|
||||
|
||||
# compress image
|
||||
echo "Compressing disk image..."
|
||||
hdiutil convert ${HDIUTIL_VERBOSITY} "${DMG_TEMP_NAME}" -format "${FORMAT}" ${IMAGEKEY} -o "${DMG_DIR}/${DMG_NAME}"
|
||||
# Compress image and optionally encrypt
|
||||
if [[ $ENABLE_ENCRYPTION -eq 0 ]]; then
|
||||
echo "Compressing disk image..."
|
||||
hdiutil convert ${HDIUTIL_VERBOSITY} "${DMG_TEMP_NAME}" -format ${FORMAT} ${IMAGEKEY} -o "${DMG_DIR}/${DMG_NAME}"
|
||||
else
|
||||
echo "Compressing and encrypting disk image..."
|
||||
echo "NOTE: hdiutil will only prompt a single time for a password - ensure entry is correct."
|
||||
hdiutil convert ${HDIUTIL_VERBOSITY} "${DMG_TEMP_NAME}" -format ${FORMAT} ${IMAGEKEY} -encryption AES-${AESBITS} -stdinpass -o "${DMG_DIR}/${DMG_NAME}"
|
||||
fi
|
||||
rm -f "${DMG_TEMP_NAME}"
|
||||
|
||||
# adding EULA resources
|
||||
# Adding EULA resources
|
||||
if [[ -n "${EULA_RSRC}" && "${EULA_RSRC}" != "-null-" ]]; then
|
||||
echo "adding EULA resources"
|
||||
echo "Adding EULA resources..."
|
||||
#
|
||||
# Use udifrez instead flatten/rez/unflatten
|
||||
# https://github.com/create-dmg/create-dmg/issues/109
|
||||
@@ -431,20 +571,19 @@ if [[ -n "${EULA_RSRC}" && "${EULA_RSRC}" != "-null-" ]]; then
|
||||
# https://developer.apple.com/forums/thread/668084
|
||||
#
|
||||
EULA_RESOURCES_FILE=$(mktemp -t createdmg.tmp.XXXXXXXXXX)
|
||||
EULA_FORMAT=$(file -b "${EULA_RSRC}")
|
||||
EULA_FORMAT=$(file -b ${EULA_RSRC})
|
||||
if [[ ${EULA_FORMAT} == 'Rich Text Format data'* ]] ; then
|
||||
EULA_FORMAT='RTF '
|
||||
else
|
||||
EULA_FORMAT='TEXT'
|
||||
fi
|
||||
|
||||
# Encode the EULA to base64
|
||||
# Replace 'openssl base64' with 'base64' if Mac OS X 10.6 support is no more needed
|
||||
# EULA_DATA="$(base64 -b 52 "${EULA_RSRC}" | sed s$'/^\(.*\)$/\t\t\t\\1/')"
|
||||
EULA_DATA="$(openssl base64 -in "${EULA_RSRC}" | tr -d '\n' | awk '{gsub(/.{52}/,"&\n")}1' | sed s$'/^\(.*\)$/\t\t\t\\1/')"
|
||||
# Fill the template with the custom EULA contents
|
||||
eval "cat > \"${EULA_RESOURCES_FILE}\" <<EOF
|
||||
$(<${AUX_PATH}/eula-resources-template.xml)
|
||||
$(<${CDMG_SUPPORT_DIR}/eula-resources-template.xml)
|
||||
EOF
|
||||
"
|
||||
# Apply the resources
|
||||
@@ -455,19 +594,45 @@ if [[ -n "${EULA_RSRC}" && "${EULA_RSRC}" != "-null-" ]]; then
|
||||
echo "Successfully added the EULA license"
|
||||
fi
|
||||
|
||||
if [[ -n "${NOINTERNET}" && "${NOINTERNET}" == 1 ]]; then
|
||||
echo "not setting 'internet-enable' on the dmg"
|
||||
# Enable "internet", whatever that is
|
||||
if [[ ! -z "${NOINTERNET}" && "${NOINTERNET}" == 1 ]]; then
|
||||
echo "Not setting 'internet-enable' on the dmg, per caller request"
|
||||
else
|
||||
# check if hdiutil supports internet-enable
|
||||
# support was removed in macOS 10.15
|
||||
# https://github.com/andreyvit/create-dmg/issues/76
|
||||
if hdiutil internet-enable -help >/dev/null 2>/dev/null
|
||||
then
|
||||
# Check if hdiutil supports internet-enable
|
||||
# Support was removed in macOS 10.15. See https://github.com/andreyvit/create-dmg/issues/76
|
||||
if hdiutil internet-enable -help >/dev/null 2>/dev/null; then
|
||||
hdiutil internet-enable -yes "${DMG_DIR}/${DMG_NAME}"
|
||||
else
|
||||
echo "hdiutil does not support internet-enable. Note it was removed in macOS 10.15."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "${SIGNATURE}" && "${SIGNATURE}" != "-null-" ]]; then
|
||||
echo "Codesign started"
|
||||
codesign -s "${SIGNATURE}" "${DMG_DIR}/${DMG_NAME}"
|
||||
dmgsignaturecheck="$(codesign --verify --deep --verbose=2 --strict "${DMG_DIR}/${DMG_NAME}" 2>&1 >/dev/null)"
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "The disk image is now codesigned"
|
||||
else
|
||||
echo "The signature seems invalid${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "${NOTARIZE}" && "${NOTARIZE}" != "-null-" ]]; then
|
||||
echo "Notarization started"
|
||||
xcrun notarytool submit "${DMG_DIR}/${DMG_NAME}" --keychain-profile "${NOTARIZE}" --wait
|
||||
echo "Stapling the notarization ticket"
|
||||
staple="$(xcrun stapler staple "${DMG_DIR}/${DMG_NAME}")"
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "The disk image is now notarized"
|
||||
else
|
||||
echo "$staple"
|
||||
echo "The notarization failed with error $?"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# All done!
|
||||
echo "Disk image done"
|
||||
exit 0
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
|
||||
use super::{app, icon::create_icns_file};
|
||||
use crate::{
|
||||
bundle::{common::CommandExt, settings::Arch, Bundle},
|
||||
bundle::{settings::Arch, Bundle},
|
||||
error::{Context, ErrorExt},
|
||||
utils::CommandExt,
|
||||
PackageType, Settings,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
use std::{
|
||||
env,
|
||||
fs::{self, write},
|
||||
@@ -48,8 +48,7 @@ pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result<
|
||||
Arch::Universal => "universal",
|
||||
target => {
|
||||
return Err(crate::Error::ArchError(format!(
|
||||
"Unsupported architecture: {:?}",
|
||||
target
|
||||
"Unsupported architecture: {target:?}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
@@ -58,20 +57,20 @@ pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result<
|
||||
let dmg_path = output_path.join(&dmg_name);
|
||||
|
||||
let product_name = settings.product_name();
|
||||
let bundle_file_name = format!("{}.app", product_name);
|
||||
let bundle_file_name = format!("{product_name}.app");
|
||||
let bundle_dir = settings.project_out_directory().join("bundle/macos");
|
||||
|
||||
let support_directory_path = output_path.join("support");
|
||||
if output_path.exists() {
|
||||
fs::remove_dir_all(&output_path)
|
||||
.with_context(|| format!("Failed to remove old {}", dmg_name))?;
|
||||
let support_directory_path = output_path
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("share/create-dmg/support");
|
||||
|
||||
for path in &[&support_directory_path, &output_path] {
|
||||
if path.exists() {
|
||||
fs::remove_dir_all(path).fs_context("failed to remove old dmg", path.to_path_buf())?;
|
||||
}
|
||||
fs::create_dir_all(path).fs_context("failed to create output directory", path.to_path_buf())?;
|
||||
}
|
||||
fs::create_dir_all(&support_directory_path).with_context(|| {
|
||||
format!(
|
||||
"Failed to create output directory at {:?}",
|
||||
support_directory_path
|
||||
)
|
||||
})?;
|
||||
|
||||
// create paths for script
|
||||
let bundle_script_path = output_path.join("bundle_dmg.sh");
|
||||
@@ -172,9 +171,11 @@ pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result<
|
||||
|
||||
// Issue #592 - Building MacOS dmg files on CI
|
||||
// https://github.com/tauri-apps/tauri/issues/592
|
||||
if let Some(value) = env::var_os("CI") {
|
||||
if value == "true" {
|
||||
bundle_dmg_cmd.arg("--skip-jenkins");
|
||||
if env::var_os("TAURI_BUNDLER_DMG_IGNORE_CI").unwrap_or_default() != "true" {
|
||||
if let Some(value) = env::var_os("CI") {
|
||||
if value == "true" {
|
||||
bundle_dmg_cmd.arg("--skip-jenkins");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,16 +191,19 @@ pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result<
|
||||
fs::rename(bundle_dir.join(dmg_name), dmg_path.clone())?;
|
||||
|
||||
// Sign DMG if needed
|
||||
|
||||
if let Some(keychain) = super::sign::keychain(settings.macos().signing_identity.as_deref())? {
|
||||
super::sign::sign(
|
||||
&keychain,
|
||||
vec![super::sign::SignTarget {
|
||||
path: dmg_path.clone(),
|
||||
is_an_executable: false,
|
||||
}],
|
||||
settings,
|
||||
)?;
|
||||
// skipping self-signing DMGs https://github.com/tauri-apps/tauri/issues/12288
|
||||
let identity = settings.macos().signing_identity.as_deref();
|
||||
if !settings.no_sign() && identity != Some("-") {
|
||||
if let Some(keychain) = super::sign::keychain(identity)? {
|
||||
super::sign::sign(
|
||||
&keychain,
|
||||
vec![super::sign::SignTarget {
|
||||
path: dmg_path.clone(),
|
||||
is_an_executable: false,
|
||||
}],
|
||||
settings,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Bundled {
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::bundle::{common, Settings};
|
||||
use crate::bundle::Settings;
|
||||
use crate::utils::{self, fs_utils, CommandExt};
|
||||
use std::{
|
||||
cmp::min,
|
||||
ffi::OsStr,
|
||||
fs::{self, File},
|
||||
io::{self, BufWriter},
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use image::GenericImageView;
|
||||
@@ -28,7 +30,7 @@ pub fn create_icns_file(out_dir: &Path, settings: &Settings) -> crate::Result<Op
|
||||
if icon_path.extension() == Some(OsStr::new("icns")) {
|
||||
let mut dest_path = out_dir.to_path_buf();
|
||||
dest_path.push(icon_path.file_name().expect("Could not get icon filename"));
|
||||
common::copy_file(&icon_path, &dest_path)?;
|
||||
fs_utils::copy_file(&icon_path, &dest_path)?;
|
||||
return Ok(Some(dest_path));
|
||||
}
|
||||
}
|
||||
@@ -62,8 +64,13 @@ pub fn create_icns_file(out_dir: &Path, settings: &Settings) -> crate::Result<Op
|
||||
let mut images_to_resize: Vec<(image::DynamicImage, u32, u32)> = vec![];
|
||||
for icon_path in settings.icon_files() {
|
||||
let icon_path = icon_path?;
|
||||
|
||||
if icon_path.extension().map_or(false, |ext| ext == "car") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let icon = image::open(&icon_path)?;
|
||||
let density = if common::is_retina(&icon_path) { 2 } else { 1 };
|
||||
let density = if utils::is_retina(&icon_path) { 2 } else { 1 };
|
||||
let (w, h) = icon.dimensions();
|
||||
let orig_size = min(w, h);
|
||||
let next_size_down = 2f32.powf((orig_size as f32).log2().floor()) as u32;
|
||||
@@ -112,3 +119,206 @@ fn make_icns_image(img: image::DynamicImage) -> io::Result<icns::Image> {
|
||||
};
|
||||
icns::Image::from_data(pixel_format, img.width(), img.height(), img.into_bytes())
|
||||
}
|
||||
|
||||
/// Creates an Assets.car file from a .icon file if there are any in the settings.
|
||||
/// Uses an existing Assets.car file if it exists in the settings.
|
||||
/// Returns the path to the Assets.car file.
|
||||
pub fn create_assets_car_file(
|
||||
out_dir: &Path,
|
||||
settings: &Settings,
|
||||
) -> crate::Result<Option<PathBuf>> {
|
||||
let Some(icons) = settings.icons() else {
|
||||
return Ok(None);
|
||||
};
|
||||
// If one of the icon files is already a CAR file, just use that.
|
||||
let mut icon_composer_icon_path = None;
|
||||
for icon in icons {
|
||||
let icon_path = Path::new(&icon).to_path_buf();
|
||||
if icon_path.extension() == Some(OsStr::new("car")) {
|
||||
let dest_path = out_dir.join("Assets.car");
|
||||
fs_utils::copy_file(&icon_path, &dest_path)?;
|
||||
return Ok(Some(dest_path));
|
||||
}
|
||||
|
||||
if icon_path.extension() == Some(OsStr::new("icon")) {
|
||||
icon_composer_icon_path.replace(icon_path);
|
||||
}
|
||||
}
|
||||
|
||||
let Some(icon_composer_icon_path) = icon_composer_icon_path else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Check actool version - must be >= 26
|
||||
if let Some(version) = get_actool_version() {
|
||||
// Parse the major version number (before the dot)
|
||||
let major_version: Option<u32> = version.split('.').next().and_then(|s| s.parse().ok());
|
||||
|
||||
if let Some(major) = major_version {
|
||||
if major < 26 {
|
||||
log::error!("actool version is less than 26, skipping Assets.car file creation. Please update Xcode to 26 or above and try again.");
|
||||
return Ok(None);
|
||||
}
|
||||
} else {
|
||||
// If we can't parse the version, return None to be safe
|
||||
log::error!("failed to parse actool version, skipping Assets.car file creation");
|
||||
return Ok(None);
|
||||
}
|
||||
} else {
|
||||
log::error!("failed to get actool version, skipping Assets.car file creation");
|
||||
// If we can't get the version, return None to be safe
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Create a temporary directory for actool work
|
||||
let temp_dir = tempfile::tempdir()
|
||||
.map_err(|e| crate::Error::GenericError(format!("failed to create temp dir: {e}")))?;
|
||||
|
||||
let icon_dest_path = temp_dir.path().join("Icon.icon");
|
||||
let output_path = temp_dir.path().join("out");
|
||||
|
||||
// Copy the input .icon directory to the temp directory
|
||||
if icon_composer_icon_path.is_dir() {
|
||||
fs_utils::copy_dir(&icon_composer_icon_path, &icon_dest_path)?;
|
||||
} else {
|
||||
return Err(crate::Error::GenericError(format!(
|
||||
"{} must be a directory",
|
||||
icon_composer_icon_path.display()
|
||||
)));
|
||||
}
|
||||
|
||||
// Create the output directory
|
||||
fs::create_dir_all(&output_path)?;
|
||||
|
||||
// Run actool command
|
||||
let mut cmd = Command::new("actool");
|
||||
cmd.arg(&icon_dest_path);
|
||||
cmd.arg("--compile");
|
||||
cmd.arg(&output_path);
|
||||
cmd.arg("--output-format");
|
||||
cmd.arg("human-readable-text");
|
||||
cmd.arg("--notices");
|
||||
cmd.arg("--warnings");
|
||||
cmd.arg("--output-partial-info-plist");
|
||||
cmd.arg(output_path.join("assetcatalog_generated_info.plist"));
|
||||
cmd.arg("--app-icon");
|
||||
cmd.arg("Icon");
|
||||
cmd.arg("--include-all-app-icons");
|
||||
cmd.arg("--accent-color");
|
||||
cmd.arg("AccentColor");
|
||||
cmd.arg("--enable-on-demand-resources");
|
||||
cmd.arg("NO");
|
||||
cmd.arg("--development-region");
|
||||
cmd.arg("en");
|
||||
cmd.arg("--target-device");
|
||||
cmd.arg("mac");
|
||||
cmd.arg("--minimum-deployment-target");
|
||||
cmd.arg("26.0");
|
||||
cmd.arg("--platform");
|
||||
cmd.arg("macosx");
|
||||
|
||||
cmd.output_ok()?;
|
||||
|
||||
let assets_car_path = output_path.join("Assets.car");
|
||||
if !assets_car_path.exists() {
|
||||
return Err(crate::Error::GenericError(
|
||||
"actool did not generate Assets.car file".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
// copy to out_dir
|
||||
fs_utils::copy_file(&assets_car_path, &out_dir.join("Assets.car"))?;
|
||||
|
||||
Ok(Some(out_dir.join("Assets.car")))
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct AssetsCarInfo {
|
||||
#[serde(rename = "AssetType", default)]
|
||||
asset_type: String,
|
||||
#[serde(rename = "Name", default)]
|
||||
name: String,
|
||||
}
|
||||
|
||||
pub fn app_icon_name_from_assets_car(assets_car_path: &Path) -> Option<String> {
|
||||
let Ok(output) = Command::new("assetutil")
|
||||
.arg("--info")
|
||||
.arg(assets_car_path)
|
||||
.output_ok()
|
||||
.inspect_err(|e| log::error!("Failed to get app icon name from Assets.car file: {e}"))
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let output = String::from_utf8(output.stdout).ok()?;
|
||||
let assets_car_info: Vec<AssetsCarInfo> = serde_json::from_str(&output)
|
||||
.inspect_err(|e| log::error!("Failed to parse Assets.car file info: {e}"))
|
||||
.ok()?;
|
||||
assets_car_info
|
||||
.iter()
|
||||
.find(|info| info.asset_type == "Icon Image")
|
||||
.map(|info| info.name.clone())
|
||||
}
|
||||
|
||||
/// Returns the actool short bundle version by running `actool --version --output-format=human-readable-text`.
|
||||
/// Returns `None` if the command fails or the output cannot be parsed.
|
||||
pub fn get_actool_version() -> Option<String> {
|
||||
let Ok(output) = Command::new("actool")
|
||||
.arg("--version")
|
||||
.arg("--output-format=human-readable-text")
|
||||
.output_ok()
|
||||
.inspect_err(|e| log::error!("Failed to get actool version: {e}"))
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let output = String::from_utf8(output.stdout).ok()?;
|
||||
parse_actool_version(&output)
|
||||
}
|
||||
|
||||
fn parse_actool_version(output: &str) -> Option<String> {
|
||||
// The output format is:
|
||||
// /* com.apple.actool.version */
|
||||
// bundle-version: 24411
|
||||
// short-bundle-version: 26.1
|
||||
for line in output.lines() {
|
||||
let line = line.trim();
|
||||
if let Some(version) = line.strip_prefix("short-bundle-version:") {
|
||||
return Some(version.trim().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_actool_version() {
|
||||
let output = r#"/* com.apple.actool.version */
|
||||
some other line
|
||||
bundle-version: 24411
|
||||
short-bundle-version: 26.1
|
||||
another line
|
||||
"#;
|
||||
|
||||
let version = parse_actool_version(output).expect("Failed to parse version");
|
||||
assert_eq!(version, "26.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_actool_version_missing_fields() {
|
||||
let output = r#"/* com.apple.actool.version */
|
||||
bundle-version: 24411
|
||||
"#;
|
||||
|
||||
assert!(parse_actool_version(output).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_actool_version_empty() {
|
||||
assert!(parse_actool_version("").is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,12 @@
|
||||
// See https://developer.apple.com/go/?id=bundle-structure for a full
|
||||
// explanation.
|
||||
|
||||
use crate::{bundle::common, Settings};
|
||||
use crate::{
|
||||
error::{Context, ErrorExt},
|
||||
utils::{self, fs_utils},
|
||||
Settings,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use image::{codecs::png::PngDecoder, GenericImageView, ImageDecoder};
|
||||
|
||||
use std::{
|
||||
@@ -42,16 +45,16 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
|
||||
if app_bundle_path.exists() {
|
||||
fs::remove_dir_all(&app_bundle_path)
|
||||
.with_context(|| format!("Failed to remove old {}", app_product_name))?;
|
||||
.fs_context("failed to remove old app bundle", &app_bundle_path)?;
|
||||
}
|
||||
fs::create_dir_all(&app_bundle_path)
|
||||
.with_context(|| format!("Failed to create bundle directory at {:?}", app_bundle_path))?;
|
||||
.fs_context("failed to create bundle directory", &app_bundle_path)?;
|
||||
|
||||
for src in settings.resource_files() {
|
||||
let src = src?;
|
||||
let dest = app_bundle_path.join(tauri_utils::resources::resource_relpath(&src));
|
||||
common::copy_file(&src, &dest)
|
||||
.with_context(|| format!("Failed to copy resource file {:?}", src))?;
|
||||
fs_utils::copy_file(&src, &dest)
|
||||
.with_context(|| format!("Failed to copy resource file {src:?}"))?;
|
||||
}
|
||||
|
||||
let icon_filenames = generate_icon_files(&app_bundle_path, settings)
|
||||
@@ -61,8 +64,8 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
|
||||
for bin in settings.binaries() {
|
||||
let bin_path = settings.binary_path(bin);
|
||||
common::copy_file(&bin_path, app_bundle_path.join(bin.name()))
|
||||
.with_context(|| format!("Failed to copy binary from {:?}", bin_path))?;
|
||||
fs_utils::copy_file(&bin_path, &app_bundle_path.join(bin.name()))
|
||||
.with_context(|| format!("Failed to copy binary from {bin_path:?}"))?;
|
||||
}
|
||||
|
||||
Ok(vec![app_bundle_path])
|
||||
@@ -93,17 +96,20 @@ fn generate_icon_files(bundle_dir: &Path, settings: &Settings) -> crate::Result<
|
||||
let decoder = PngDecoder::new(BufReader::new(File::open(&icon_path)?))?;
|
||||
let width = decoder.dimensions().0;
|
||||
let height = decoder.dimensions().1;
|
||||
let is_retina = common::is_retina(&icon_path);
|
||||
let is_retina = utils::is_retina(&icon_path);
|
||||
if !sizes.contains(&(width, height, is_retina)) {
|
||||
sizes.insert((width, height, is_retina));
|
||||
let dest_path = get_dest_path(width, height, is_retina);
|
||||
common::copy_file(&icon_path, &dest_path)?;
|
||||
fs_utils::copy_file(&icon_path, &dest_path)?;
|
||||
}
|
||||
}
|
||||
// Fall back to non-PNG files for any missing sizes.
|
||||
for icon_path in settings.icon_files() {
|
||||
let icon_path = icon_path?;
|
||||
if icon_path.extension() == Some(OsStr::new("png")) {
|
||||
if icon_path
|
||||
.extension()
|
||||
.map_or(false, |ext| ext == "png" || ext == "car")
|
||||
{
|
||||
continue;
|
||||
} else if icon_path.extension() == Some(OsStr::new("icns")) {
|
||||
let icon_family = icns::IconFamily::read(File::open(&icon_path)?)?;
|
||||
@@ -121,12 +127,12 @@ fn generate_icon_files(bundle_dir: &Path, settings: &Settings) -> crate::Result<
|
||||
} else {
|
||||
let icon = image::open(&icon_path)?;
|
||||
let (width, height) = icon.dimensions();
|
||||
let is_retina = common::is_retina(&icon_path);
|
||||
let is_retina = utils::is_retina(&icon_path);
|
||||
if !sizes.contains(&(width, height, is_retina)) {
|
||||
sizes.insert((width, height, is_retina));
|
||||
let dest_path = get_dest_path(width, height, is_retina);
|
||||
icon.write_to(
|
||||
&mut common::create_file(&dest_path)?,
|
||||
&mut fs_utils::create_file(&dest_path)?,
|
||||
image::ImageFormat::Png,
|
||||
)?;
|
||||
}
|
||||
@@ -142,7 +148,7 @@ fn generate_info_plist(
|
||||
settings: &Settings,
|
||||
icon_filenames: &[String],
|
||||
) -> crate::Result<()> {
|
||||
let file = &mut common::create_file(&bundle_dir.join("Info.plist"))?;
|
||||
let file = &mut fs_utils::create_file(&bundle_dir.join("Info.plist"))?;
|
||||
writeln!(
|
||||
file,
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
|
||||
@@ -175,7 +181,11 @@ fn generate_info_plist(
|
||||
writeln!(
|
||||
file,
|
||||
" <key>CFBundleVersion</key>\n <string>{}</string>",
|
||||
settings.version_string()
|
||||
settings
|
||||
.ios()
|
||||
.bundle_version
|
||||
.as_deref()
|
||||
.unwrap_or_else(|| settings.version_string())
|
||||
)?;
|
||||
writeln!(
|
||||
file,
|
||||
@@ -190,7 +200,7 @@ fn generate_info_plist(
|
||||
if !icon_filenames.is_empty() {
|
||||
writeln!(file, " <key>CFBundleIconFiles</key>\n <array>")?;
|
||||
for filename in icon_filenames {
|
||||
writeln!(file, " <string>{}</string>", filename)?;
|
||||
writeln!(file, " <string>{filename}</string>")?;
|
||||
}
|
||||
writeln!(file, " </array>")?;
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
use std::{
|
||||
env::{var, var_os},
|
||||
ffi::OsString,
|
||||
path::{Path, PathBuf},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use crate::Settings;
|
||||
use crate::{error::NotarizeAuthError, Entitlements, Settings};
|
||||
|
||||
pub struct SignTarget {
|
||||
pub path: PathBuf,
|
||||
@@ -21,13 +21,16 @@ pub fn keychain(identity: Option<&str>) -> crate::Result<Option<tauri_macos_sign
|
||||
var_os("APPLE_CERTIFICATE"),
|
||||
var_os("APPLE_CERTIFICATE_PASSWORD"),
|
||||
) {
|
||||
// import user certificate - useful for for CI build
|
||||
// import user certificate - useful for CI build
|
||||
let keychain =
|
||||
tauri_macos_sign::Keychain::with_certificate(&certificate_encoded, &certificate_password)?;
|
||||
tauri_macos_sign::Keychain::with_certificate(&certificate_encoded, &certificate_password)
|
||||
.map_err(Box::new)?;
|
||||
if let Some(identity) = identity {
|
||||
let certificate_identity = keychain.signing_identity();
|
||||
if !certificate_identity.contains(identity) {
|
||||
return Err(anyhow::anyhow!("certificate from APPLE_CERTIFICATE \"{certificate_identity}\" environment variable does not match provided identity \"{identity}\"").into());
|
||||
return Err(crate::Error::GenericError(format!(
|
||||
"certificate from APPLE_CERTIFICATE \"{certificate_identity}\" environment variable does not match provided identity \"{identity}\""
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(Some(keychain))
|
||||
@@ -48,11 +51,23 @@ pub fn sign(
|
||||
log::info!(action = "Signing"; "with identity \"{}\"", keychain.signing_identity());
|
||||
|
||||
for target in targets {
|
||||
keychain.sign(
|
||||
&target.path,
|
||||
settings.macos().entitlements.as_ref().map(Path::new),
|
||||
target.is_an_executable && settings.macos().hardened_runtime,
|
||||
)?;
|
||||
let (entitlements_path, _temp_file) = match settings.macos().entitlements.as_ref() {
|
||||
Some(Entitlements::Path(path)) => (Some(path.to_owned()), None),
|
||||
Some(Entitlements::Plist(plist)) => {
|
||||
let mut temp_file = tempfile::NamedTempFile::new()?;
|
||||
plist::to_writer_xml(temp_file.as_file_mut(), &plist)?;
|
||||
(Some(temp_file.path().to_path_buf()), Some(temp_file))
|
||||
}
|
||||
None => (None, None),
|
||||
};
|
||||
|
||||
keychain
|
||||
.sign(
|
||||
&target.path,
|
||||
entitlements_path.as_deref(),
|
||||
target.is_an_executable && settings.macos().hardened_runtime,
|
||||
)
|
||||
.map_err(Box::new)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -63,17 +78,19 @@ pub fn notarize(
|
||||
app_bundle_path: PathBuf,
|
||||
credentials: &tauri_macos_sign::AppleNotarizationCredentials,
|
||||
) -> crate::Result<()> {
|
||||
tauri_macos_sign::notarize(keychain, &app_bundle_path, credentials).map_err(Into::into)
|
||||
tauri_macos_sign::notarize(keychain, &app_bundle_path, credentials)
|
||||
.map_err(Box::new)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum NotarizeAuthError {
|
||||
#[error(
|
||||
"The team ID is now required for notarization with app-specific password as authentication. Please set the `APPLE_TEAM_ID` environment variable. You can find the team ID in https://developer.apple.com/account#MembershipDetailsCard."
|
||||
)]
|
||||
MissingTeamId,
|
||||
#[error(transparent)]
|
||||
Anyhow(#[from] anyhow::Error),
|
||||
pub fn notarize_without_stapling(
|
||||
keychain: &tauri_macos_sign::Keychain,
|
||||
app_bundle_path: PathBuf,
|
||||
credentials: &tauri_macos_sign::AppleNotarizationCredentials,
|
||||
) -> crate::Result<()> {
|
||||
tauri_macos_sign::notarize_without_stapling(keychain, &app_bundle_path, credentials)
|
||||
.map_err(Box::new)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn notarize_auth() -> Result<tauri_macos_sign::AppleNotarizationCredentials, NotarizeAuthError>
|
||||
@@ -92,10 +109,18 @@ pub fn notarize_auth() -> Result<tauri_macos_sign::AppleNotarizationCredentials,
|
||||
}
|
||||
(Some(_apple_id), Some(_password), None) => Err(NotarizeAuthError::MissingTeamId),
|
||||
_ => {
|
||||
match (var_os("APPLE_API_KEY"), var_os("APPLE_API_ISSUER"), var("APPLE_API_KEY_PATH")) {
|
||||
match (
|
||||
var_os("APPLE_API_KEY"),
|
||||
var_os("APPLE_API_ISSUER"),
|
||||
var("APPLE_API_KEY_PATH"),
|
||||
) {
|
||||
(Some(key_id), Some(issuer), Ok(key_path)) => {
|
||||
Ok(tauri_macos_sign::AppleNotarizationCredentials::ApiKey { key_id, key: tauri_macos_sign::ApiKey::Path( key_path.into()), issuer })
|
||||
},
|
||||
Ok(tauri_macos_sign::AppleNotarizationCredentials::ApiKey {
|
||||
key_id,
|
||||
key: tauri_macos_sign::ApiKey::Path(key_path.into()),
|
||||
issuer,
|
||||
})
|
||||
}
|
||||
(Some(key_id), Some(issuer), Err(_)) => {
|
||||
let mut api_key_file_name = OsString::from("AuthKey_");
|
||||
api_key_file_name.push(&key_id);
|
||||
@@ -117,12 +142,18 @@ pub fn notarize_auth() -> Result<tauri_macos_sign::AppleNotarizationCredentials,
|
||||
}
|
||||
|
||||
if let Some(key_path) = key_path {
|
||||
Ok(tauri_macos_sign::AppleNotarizationCredentials::ApiKey { key_id, key: tauri_macos_sign::ApiKey::Path(key_path), issuer })
|
||||
Ok(tauri_macos_sign::AppleNotarizationCredentials::ApiKey {
|
||||
key_id,
|
||||
key: tauri_macos_sign::ApiKey::Path(key_path),
|
||||
issuer,
|
||||
})
|
||||
} else {
|
||||
Err(anyhow::anyhow!("could not find API key file. Please set the APPLE_API_KEY_PATH environment variables to the path to the {api_key_file_name:?} file").into())
|
||||
Err(NotarizeAuthError::MissingApiKey {
|
||||
file_name: api_key_file_name.to_string_lossy().into_owned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
_ => Err(anyhow::anyhow!("no APPLE_ID & APPLE_PASSWORD & APPLE_TEAM_ID or APPLE_API_KEY & APPLE_API_ISSUER & APPLE_API_KEY_PATH environment variables found").into())
|
||||
_ => Err(NotarizeAuthError::MissingCredentials),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
// Copyright 2016-2019 Cargo-Bundle developers <https://github.com/burtonageo/cargo-bundle>
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{
|
||||
fs::{create_dir, create_dir_all, read_dir, remove_dir_all},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
/// Directory options.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct DirOpts {
|
||||
pub depth: u64,
|
||||
}
|
||||
|
||||
/// File options.
|
||||
pub struct FileOpts {
|
||||
pub overwrite: bool,
|
||||
pub skip: bool,
|
||||
#[allow(dead_code)]
|
||||
pub buffer_size: usize,
|
||||
}
|
||||
|
||||
/// Copy options.
|
||||
#[derive(Clone)]
|
||||
pub struct Options {
|
||||
pub overwrite: bool,
|
||||
pub skip: bool,
|
||||
pub buffer_size: usize,
|
||||
pub copy_files: bool,
|
||||
pub content_only: bool,
|
||||
pub depth: u64,
|
||||
}
|
||||
|
||||
/// Directory information descriptor
|
||||
pub struct DirInfo {
|
||||
pub size: u64,
|
||||
pub files: Vec<String>,
|
||||
pub directories: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for Options {
|
||||
fn default() -> Options {
|
||||
Options {
|
||||
overwrite: false,
|
||||
skip: false,
|
||||
buffer_size: 64000,
|
||||
copy_files: false,
|
||||
content_only: false,
|
||||
depth: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FileOpts {
|
||||
fn default() -> FileOpts {
|
||||
FileOpts {
|
||||
overwrite: false,
|
||||
skip: false,
|
||||
buffer_size: 64000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates the given directory path,
|
||||
/// erasing it first if specified.
|
||||
pub fn create<P>(path: P, erase: bool) -> crate::Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
if erase && path.as_ref().exists() {
|
||||
remove(&path)?;
|
||||
}
|
||||
Ok(create_dir(&path)?)
|
||||
}
|
||||
|
||||
/// Creates all of the directories of the specified path,
|
||||
/// erasing it first if specified.
|
||||
pub fn create_all<P>(path: P, erase: bool) -> crate::Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
if erase && path.as_ref().exists() {
|
||||
remove(&path)?;
|
||||
}
|
||||
Ok(create_dir_all(&path)?)
|
||||
}
|
||||
|
||||
/// Removes the directory if it exists.
|
||||
pub fn remove<P: AsRef<Path>>(path: P) -> crate::Result<()> {
|
||||
if path.as_ref().exists() {
|
||||
Ok(remove_dir_all(path)?)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy file with the given options.
|
||||
pub fn copy_file<P, Q>(from: P, to: Q, options: &FileOpts) -> crate::Result<u64>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
Q: AsRef<Path>,
|
||||
{
|
||||
let from = from.as_ref();
|
||||
if !from.exists() {
|
||||
if let Some(msg) = from.to_str() {
|
||||
let msg = format!("Path \"{msg}\" does not exist or you don't have access");
|
||||
return Err(crate::Error::PathUtilError(msg));
|
||||
}
|
||||
return Err(crate::Error::PathUtilError(
|
||||
"Path does not exist or you don't have access!".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
if !from.is_file() {
|
||||
if let Some(msg) = from.to_str() {
|
||||
let msg = format!("Path \"{msg}\" is not a file!");
|
||||
return Err(crate::Error::PathUtilError(msg));
|
||||
}
|
||||
return Err(crate::Error::PathUtilError(
|
||||
"Path is not a file!".to_owned(),
|
||||
));
|
||||
}
|
||||
if !options.overwrite && to.as_ref().exists() {
|
||||
if options.skip {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
if let Some(msg) = to.as_ref().to_str() {
|
||||
let msg = format!("Path \"{msg}\" is exist");
|
||||
return Err(crate::Error::PathUtilError(msg));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(std::fs::copy(from, to)?)
|
||||
}
|
||||
|
||||
/// Copies the directory with the given options.
|
||||
#[allow(dead_code)]
|
||||
pub fn copy<P, Q>(from: P, to: Q, options: &Options) -> crate::Result<u64>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
Q: AsRef<Path>,
|
||||
{
|
||||
let from = from.as_ref();
|
||||
if !from.exists() {
|
||||
if let Some(msg) = from.to_str() {
|
||||
let msg = format!("Path \"{msg}\" does not exist or you don't have access!");
|
||||
return Err(crate::Error::PathUtilError(msg));
|
||||
}
|
||||
return Err(crate::Error::PathUtilError(
|
||||
"Path does not exist or you don't have access".to_owned(),
|
||||
));
|
||||
}
|
||||
if !from.is_dir() {
|
||||
if let Some(msg) = from.to_str() {
|
||||
let msg = format!("Path \"{msg}\" is not a directory!");
|
||||
return Err(crate::Error::PathUtilError(msg));
|
||||
}
|
||||
return Err(crate::Error::PathUtilError(
|
||||
"Path is not a directory".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
let dir_name = if let Some(val) = from.components().last() {
|
||||
val.as_os_str()
|
||||
} else {
|
||||
return Err(crate::Error::PathUtilError(
|
||||
"Invalid Folder form".to_owned(),
|
||||
));
|
||||
};
|
||||
let mut to: PathBuf = to.as_ref().to_path_buf();
|
||||
if !options.content_only && (!options.copy_files || to.exists()) {
|
||||
to.push(dir_name);
|
||||
}
|
||||
|
||||
let mut read_options = DirOpts::default();
|
||||
if options.depth > 0 {
|
||||
read_options.depth = options.depth;
|
||||
}
|
||||
|
||||
let dir_content = get_dir_info(from, &read_options)?;
|
||||
for directory in dir_content.directories {
|
||||
let tmp_to = Path::new(&directory).strip_prefix(from)?;
|
||||
let dir = to.join(tmp_to);
|
||||
if !dir.exists() {
|
||||
if options.copy_files {
|
||||
create_all(dir, false)?;
|
||||
} else {
|
||||
create(dir, false)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut result: u64 = 0;
|
||||
for file in dir_content.files {
|
||||
let to = to.to_path_buf();
|
||||
let tp = Path::new(&file).strip_prefix(from)?;
|
||||
let path = to.join(tp);
|
||||
|
||||
let file_options = FileOpts {
|
||||
overwrite: options.overwrite,
|
||||
skip: options.skip,
|
||||
buffer_size: options.buffer_size,
|
||||
};
|
||||
let mut result_copy: crate::Result<u64>;
|
||||
let mut work = true;
|
||||
|
||||
while work {
|
||||
#[allow(clippy::needless_borrow)]
|
||||
{
|
||||
result_copy = copy_file(&file, &path, &file_options);
|
||||
}
|
||||
match result_copy {
|
||||
Ok(val) => {
|
||||
result += val;
|
||||
work = false;
|
||||
}
|
||||
Err(err) => {
|
||||
let err_msg = err.to_string();
|
||||
return Err(crate::Error::PathUtilError(err_msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Gets the DirInfo from the directory path with the given options.
|
||||
pub fn get_dir_info<P>(path: P, options: &DirOpts) -> crate::Result<DirInfo>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let depth = if options.depth == 0 {
|
||||
0
|
||||
} else {
|
||||
options.depth + 1
|
||||
};
|
||||
|
||||
_get_dir_info(path, depth)
|
||||
}
|
||||
|
||||
/// Gets the DirInfo from the directory with the given depth.
|
||||
fn _get_dir_info<P>(path: P, mut depth: u64) -> crate::Result<DirInfo>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let mut directories = Vec::new();
|
||||
let mut files = Vec::new();
|
||||
let mut size = 0;
|
||||
let item = path.as_ref().to_str();
|
||||
if item.is_none() {
|
||||
return Err(crate::Error::PathUtilError("Invalid Path".to_owned()));
|
||||
}
|
||||
let item = item.expect("Item had no data").to_string();
|
||||
|
||||
if path.as_ref().is_dir() {
|
||||
directories.push(item);
|
||||
if depth == 0 || depth > 1 {
|
||||
if depth > 1 {
|
||||
depth -= 1;
|
||||
}
|
||||
for entry in read_dir(&path)? {
|
||||
let _path = entry?.path();
|
||||
|
||||
match _get_dir_info(_path, depth) {
|
||||
Ok(items) => {
|
||||
let mut _files = items.files;
|
||||
let mut _directories = items.directories;
|
||||
size += items.size;
|
||||
files.append(&mut _files);
|
||||
directories.append(&mut _directories);
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
size = path.as_ref().metadata()?.len();
|
||||
files.push(item);
|
||||
}
|
||||
Ok(DirInfo {
|
||||
size,
|
||||
files,
|
||||
directories,
|
||||
})
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use super::common::CommandExt;
|
||||
use crate::utils::CommandExt;
|
||||
use std::process::Command;
|
||||
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
@@ -57,6 +57,8 @@ pub fn target_triple() -> Result<String, crate::Error> {
|
||||
"armv7".into()
|
||||
} else if cfg!(target_arch = "aarch64") {
|
||||
"aarch64".into()
|
||||
} else if cfg!(target_arch = "riscv64") {
|
||||
"riscv64".into()
|
||||
} else {
|
||||
return Err(crate::Error::ArchError(String::from(
|
||||
"Unable to determine target-architecture",
|
||||
|
||||
@@ -4,11 +4,14 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use super::category::AppCategory;
|
||||
use crate::bundle::{common, platform::target_triple};
|
||||
use anyhow::Context;
|
||||
use crate::{bundle::platform::target_triple, error::Context, utils::fs_utils};
|
||||
pub use tauri_utils::config::WebviewInstallMode;
|
||||
use tauri_utils::{
|
||||
config::{BundleType, DeepLinkProtocol, FileAssociation, NSISInstallerMode, NsisCompression},
|
||||
config::{
|
||||
BundleType, DeepLinkProtocol, FileAssociation, NSISInstallerMode, NsisCompression,
|
||||
RpmCompression,
|
||||
},
|
||||
platform::Target as TargetPlatform,
|
||||
resources::{external_binaries, ResourcePaths},
|
||||
};
|
||||
|
||||
@@ -123,7 +126,7 @@ const ALL_PACKAGE_TYPES: &[PackageType] = &[
|
||||
PackageType::IosBundle,
|
||||
#[cfg(target_os = "windows")]
|
||||
PackageType::WindowsMsi,
|
||||
#[cfg(target_os = "windows")]
|
||||
// NSIS installers can be built on all platforms but it's hidden in the --help output on macOS/Linux.
|
||||
PackageType::Nsis,
|
||||
#[cfg(target_os = "macos")]
|
||||
PackageType::MacOsBundle,
|
||||
@@ -170,6 +173,8 @@ pub struct DebianSettings {
|
||||
// OS-specific settings:
|
||||
/// the list of debian dependencies.
|
||||
pub depends: Option<Vec<String>>,
|
||||
/// the list of debian dependencies recommendations.
|
||||
pub recommends: Option<Vec<String>>,
|
||||
/// the list of dependencies the package provides.
|
||||
pub provides: Option<Vec<String>>,
|
||||
/// the list of package conflicts.
|
||||
@@ -215,6 +220,10 @@ pub struct DebianSettings {
|
||||
pub struct AppImageSettings {
|
||||
/// The files to include in the Appimage Binary.
|
||||
pub files: HashMap<PathBuf, PathBuf>,
|
||||
/// Whether to include gstreamer plugins for audio/media support.
|
||||
pub bundle_media_framework: bool,
|
||||
/// Whether to include the `xdg-open` binary.
|
||||
pub bundle_xdg_open: bool,
|
||||
}
|
||||
|
||||
/// The RPM bundle settings.
|
||||
@@ -222,13 +231,15 @@ pub struct AppImageSettings {
|
||||
pub struct RpmSettings {
|
||||
/// The list of RPM dependencies your application relies on.
|
||||
pub depends: Option<Vec<String>>,
|
||||
/// the list of RPM dependencies your application recommends.
|
||||
pub recommends: Option<Vec<String>>,
|
||||
/// The list of RPM dependencies your application provides.
|
||||
pub provides: Option<Vec<String>>,
|
||||
/// The list of RPM dependencies your application conflicts with. They must not be present
|
||||
/// in order for the package to be installed.
|
||||
pub conflicts: Option<Vec<String>>,
|
||||
/// The list of RPM dependencies your application supersedes - if this package is installed,
|
||||
/// packages listed as “obsoletes” will be automatically removed (if they are present).
|
||||
/// packages listed as "obsoletes" will be automatically removed (if they are present).
|
||||
pub obsoletes: Option<Vec<String>>,
|
||||
/// The RPM release tag.
|
||||
pub release: String,
|
||||
@@ -258,6 +269,8 @@ pub struct RpmSettings {
|
||||
/// Path to script that will be executed after the package is removed. See
|
||||
/// <http://ftp.rpm.org/max-rpm/s1-rpm-inside-scripts.html>
|
||||
pub post_remove_script: Option<PathBuf>,
|
||||
/// Compression algorithm and level. Defaults to `Gzip` with level 6.
|
||||
pub compression: Option<RpmCompression>,
|
||||
}
|
||||
|
||||
/// Position coordinates struct.
|
||||
@@ -293,6 +306,13 @@ pub struct DmgSettings {
|
||||
pub application_folder_position: Position,
|
||||
}
|
||||
|
||||
/// The iOS bundle settings.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct IosSettings {
|
||||
/// The version of the build that identifies an iteration of the bundle.
|
||||
pub bundle_version: Option<String>,
|
||||
}
|
||||
|
||||
/// The macOS bundle settings.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MacOsSettings {
|
||||
@@ -310,6 +330,12 @@ pub struct MacOsSettings {
|
||||
/// List of custom files to add to the application bundle.
|
||||
/// Maps the path in the Contents directory in the app to the path of the file to include (relative to the current working directory).
|
||||
pub files: HashMap<PathBuf, PathBuf>,
|
||||
/// The version of the build that identifies an iteration of the bundle.
|
||||
pub bundle_version: Option<String>,
|
||||
/// The name of the build that identifies a string of the bundle.
|
||||
///
|
||||
/// If not set, defaults to the package's product name.
|
||||
pub bundle_name: Option<String>,
|
||||
/// A version string indicating the minimum MacOS version that the bundled app supports (e.g. `"10.11"`).
|
||||
/// If you are using this config field, you may also want have your `build.rs` script emit `cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.11`.
|
||||
pub minimum_system_version: Option<String>,
|
||||
@@ -319,16 +345,43 @@ pub struct MacOsSettings {
|
||||
pub exception_domain: Option<String>,
|
||||
/// Code signing identity.
|
||||
pub signing_identity: Option<String>,
|
||||
/// Whether to wait for notarization to finish and `staple` the ticket onto the app.
|
||||
///
|
||||
/// Gatekeeper will look for stapled tickets to tell whether your app was notarized without
|
||||
/// reaching out to Apple's servers which is helpful in offline environments.
|
||||
///
|
||||
/// Enabling this option will also result in `tauri build` not waiting for notarization to finish
|
||||
/// which is helpful for the very first time your app is notarized as this can take multiple hours.
|
||||
/// On subsequent runs, it's recommended to disable this setting again.
|
||||
pub skip_stapling: bool,
|
||||
/// Preserve the hardened runtime version flag, see <https://developer.apple.com/documentation/security/hardened_runtime>
|
||||
///
|
||||
/// Settings this to `false` is useful when using an ad-hoc signature, making it less strict.
|
||||
pub hardened_runtime: bool,
|
||||
/// Provider short name for notarization.
|
||||
pub provider_short_name: Option<String>,
|
||||
/// Path or contents of the entitlements.plist file.
|
||||
pub entitlements: Option<Entitlements>,
|
||||
/// Path to the Info.plist file or raw plist value to merge with the bundle Info.plist.
|
||||
pub info_plist: Option<PlistKind>,
|
||||
}
|
||||
|
||||
/// Entitlements for macOS code signing.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Entitlements {
|
||||
/// Path to the entitlements.plist file.
|
||||
pub entitlements: Option<String>,
|
||||
/// Path to the Info.plist file for the bundle.
|
||||
pub info_plist_path: Option<PathBuf>,
|
||||
Path(PathBuf),
|
||||
/// Raw plist::Value.
|
||||
Plist(plist::Value),
|
||||
}
|
||||
|
||||
/// Plist format.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PlistKind {
|
||||
/// Path to a .plist file.
|
||||
Path(PathBuf),
|
||||
/// Raw plist value.
|
||||
Plist(plist::Value),
|
||||
}
|
||||
|
||||
/// Configuration for a target language for the WiX build.
|
||||
@@ -351,6 +404,15 @@ impl Default for WixLanguage {
|
||||
/// Settings specific to the WiX implementation.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct WixSettings {
|
||||
/// MSI installer version in the format `major.minor.patch.build` (build is optional).
|
||||
///
|
||||
/// Because a valid version is required for MSI installer, it will be derived from [`PackageSettings::version`] if this field is not set.
|
||||
///
|
||||
/// The first field is the major version and has a maximum value of 255. The second field is the minor version and has a maximum value of 255.
|
||||
/// The third and fourth fields have a maximum value of 65,535.
|
||||
///
|
||||
/// See <https://learn.microsoft.com/en-us/windows/win32/msi/productversion> for more info.
|
||||
pub version: Option<String>,
|
||||
/// A GUID upgrade code for MSI installer. This code **_must stay the same across all of your updates_**,
|
||||
/// otherwise, Windows will treat your update as a different app and your users will have duplicate versions of your app.
|
||||
///
|
||||
@@ -386,7 +448,7 @@ pub struct WixSettings {
|
||||
pub banner_path: Option<PathBuf>,
|
||||
/// Path to a bitmap file to use on the installation user interface dialogs.
|
||||
/// It is used on the welcome and completion dialogs.
|
||||
|
||||
///
|
||||
/// The required dimensions are 493px × 312px.
|
||||
pub dialog_image_path: Option<PathBuf>,
|
||||
/// Enables FIPS compliant algorithms.
|
||||
@@ -408,6 +470,13 @@ pub struct NsisSettings {
|
||||
pub sidebar_image: Option<PathBuf>,
|
||||
/// The path to an icon file used as the installer icon.
|
||||
pub installer_icon: Option<PathBuf>,
|
||||
/// The path to an icon file used as the uninstaller icon.
|
||||
pub uninstaller_icon: Option<PathBuf>,
|
||||
/// The path to a bitmap file to display on the header of uninstallers pages.
|
||||
/// Defaults to [`Self::header_image`]. If this is set but [`Self::header_image`] is not, a default image from NSIS will be applied to `header_image`
|
||||
///
|
||||
/// The recommended dimensions are 150px x 57px.
|
||||
pub uninstaller_header_image: Option<PathBuf>,
|
||||
/// Whether the installation will be for all users or just the current user.
|
||||
pub install_mode: NSISInstallerMode,
|
||||
/// A list of installer languages.
|
||||
@@ -470,6 +539,10 @@ pub struct NsisSettings {
|
||||
/// Try to ensure that the WebView2 version is equal to or newer than this version,
|
||||
/// if the user's WebView2 is older than this version,
|
||||
/// the installer will try to trigger a WebView2 update.
|
||||
#[deprecated(
|
||||
since = "2.8.0",
|
||||
note = "Use `WindowsSettings::minimum_webview2_version` instead."
|
||||
)]
|
||||
pub minimum_webview2_version: Option<String>,
|
||||
}
|
||||
|
||||
@@ -501,6 +574,7 @@ pub struct WindowsSettings {
|
||||
/// Nsis configuration.
|
||||
pub nsis: Option<NsisSettings>,
|
||||
/// The path to the application icon. Defaults to `./icons/icon.ico`.
|
||||
#[deprecated = "This is used for the MSI installer and will be removed in 3.0.0, use `BundleSettings::icon` field and make sure a `.ico` icon exists instead."]
|
||||
pub icon_path: PathBuf,
|
||||
/// The installation mode for the Webview2 runtime.
|
||||
pub webview_install_mode: WebviewInstallMode,
|
||||
@@ -524,21 +598,37 @@ pub struct WindowsSettings {
|
||||
/// if you are on another platform and want to cross-compile and sign you will
|
||||
/// need to use another tool like `osslsigncode`.
|
||||
pub sign_command: Option<CustomSignCommandSettings>,
|
||||
/// Try to ensure that the WebView2 version is equal to or newer than this version,
|
||||
/// if the user's WebView2 is older than this version,
|
||||
/// the installer will try to trigger a WebView2 update.
|
||||
pub minimum_webview2_version: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for WindowsSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
digest_algorithm: None,
|
||||
certificate_thumbprint: None,
|
||||
timestamp_url: None,
|
||||
tsp: false,
|
||||
wix: None,
|
||||
nsis: None,
|
||||
icon_path: PathBuf::from("icons/icon.ico"),
|
||||
webview_install_mode: Default::default(),
|
||||
allow_downgrades: true,
|
||||
sign_command: None,
|
||||
impl WindowsSettings {
|
||||
pub(crate) fn can_sign(&self) -> bool {
|
||||
self.sign_command.is_some() || self.certificate_thumbprint.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
mod _default {
|
||||
use super::*;
|
||||
|
||||
impl Default for WindowsSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
digest_algorithm: None,
|
||||
certificate_thumbprint: None,
|
||||
timestamp_url: None,
|
||||
tsp: false,
|
||||
wix: None,
|
||||
nsis: None,
|
||||
icon_path: PathBuf::from("icons/icon.ico"),
|
||||
webview_install_mode: Default::default(),
|
||||
allow_downgrades: true,
|
||||
sign_command: None,
|
||||
minimum_webview2_version: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -615,6 +705,8 @@ pub struct BundleSettings {
|
||||
pub rpm: RpmSettings,
|
||||
/// DMG-specific settings.
|
||||
pub dmg: DmgSettings,
|
||||
/// iOS-specific settings.
|
||||
pub ios: IosSettings,
|
||||
/// MacOS-specific settings.
|
||||
pub macos: MacOsSettings,
|
||||
/// Updater configuration.
|
||||
@@ -695,6 +787,8 @@ pub enum Arch {
|
||||
Armhf,
|
||||
/// For the AArch32 / ARM32 instruction sets with soft-float (32 bits).
|
||||
Armel,
|
||||
/// For the RISC-V instruction sets (64 bits).
|
||||
Riscv64,
|
||||
/// For universal macOS applications.
|
||||
Universal,
|
||||
}
|
||||
@@ -717,10 +811,16 @@ pub struct Settings {
|
||||
local_tools_directory: Option<PathBuf>,
|
||||
/// the bundle settings.
|
||||
bundle_settings: BundleSettings,
|
||||
/// Same as `bundle_settings.icon`, but without the .icon directory.
|
||||
icon_files: Option<Vec<String>>,
|
||||
/// the binaries to bundle.
|
||||
binaries: Vec<BundleBinary>,
|
||||
/// The target platform.
|
||||
target_platform: TargetPlatform,
|
||||
/// The target triple.
|
||||
target: String,
|
||||
/// Whether to disable code signing during the bundling process.
|
||||
no_sign: bool,
|
||||
}
|
||||
|
||||
/// A builder for [`Settings`].
|
||||
@@ -734,6 +834,7 @@ pub struct SettingsBuilder {
|
||||
binaries: Vec<BundleBinary>,
|
||||
target: Option<String>,
|
||||
local_tools_directory: Option<PathBuf>,
|
||||
no_sign: bool,
|
||||
}
|
||||
|
||||
impl SettingsBuilder {
|
||||
@@ -803,6 +904,13 @@ impl SettingsBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether to skip code signing.
|
||||
#[must_use]
|
||||
pub fn no_sign(mut self, no_sign: bool) -> Self {
|
||||
self.no_sign = no_sign;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builds a Settings from the CLI args.
|
||||
///
|
||||
/// Package settings will be read from Cargo.toml.
|
||||
@@ -814,6 +922,15 @@ impl SettingsBuilder {
|
||||
} else {
|
||||
target_triple()?
|
||||
};
|
||||
let target_platform = TargetPlatform::from_triple(&target);
|
||||
|
||||
let icon_files = self.bundle_settings.icon.as_ref().map(|paths| {
|
||||
paths
|
||||
.iter()
|
||||
.filter(|p| !p.ends_with(".icon"))
|
||||
.cloned()
|
||||
.collect()
|
||||
});
|
||||
|
||||
Ok(Settings {
|
||||
log_level: self.log_level.unwrap_or(log::Level::Error),
|
||||
@@ -831,10 +948,13 @@ impl SettingsBuilder {
|
||||
.bundle_settings
|
||||
.external_bin
|
||||
.as_ref()
|
||||
.map(|bins| external_binaries(bins, &target)),
|
||||
.map(|bins| external_binaries(bins, &target, &target_platform)),
|
||||
..self.bundle_settings
|
||||
},
|
||||
icon_files,
|
||||
target_platform,
|
||||
target,
|
||||
no_sign: self.no_sign,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -860,6 +980,16 @@ impl Settings {
|
||||
&self.target
|
||||
}
|
||||
|
||||
/// Returns the [`TargetPlatform`].
|
||||
pub fn target_platform(&self) -> &TargetPlatform {
|
||||
&self.target_platform
|
||||
}
|
||||
|
||||
/// Raw list of icons.
|
||||
pub fn icons(&self) -> Option<&Vec<String>> {
|
||||
self.bundle_settings.icon.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the architecture for the binary being bundled (e.g. "arm", "x86" or "x86_64").
|
||||
pub fn binary_arch(&self) -> Arch {
|
||||
if self.target.starts_with("x86_64") {
|
||||
@@ -872,6 +1002,8 @@ impl Settings {
|
||||
Arch::Armel
|
||||
} else if self.target.starts_with("aarch64") {
|
||||
Arch::AArch64
|
||||
} else if self.target.starts_with("riscv64") {
|
||||
Arch::Riscv64
|
||||
} else if self.target.starts_with("universal") {
|
||||
Arch::Universal
|
||||
} else {
|
||||
@@ -886,7 +1018,15 @@ impl Settings {
|
||||
.iter()
|
||||
.find(|bin| bin.main)
|
||||
.context("failed to find main binary, make sure you have a `package > default-run` in the Cargo.toml file")
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Returns the file name of the binary being bundled.
|
||||
pub fn main_binary_mut(&mut self) -> crate::Result<&mut BundleBinary> {
|
||||
self
|
||||
.binaries
|
||||
.iter_mut()
|
||||
.find(|bin| bin.main)
|
||||
.context("failed to find main binary, make sure you have a `package > default-run` in the Cargo.toml file")
|
||||
}
|
||||
|
||||
/// Returns the file name of the binary being bundled.
|
||||
@@ -897,24 +1037,27 @@ impl Settings {
|
||||
.find(|bin| bin.main)
|
||||
.context("failed to find main binary, make sure you have a `package > default-run` in the Cargo.toml file")
|
||||
.map(|b| b.name())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Returns the path to the specified binary.
|
||||
pub fn binary_path(&self, binary: &BundleBinary) -> PathBuf {
|
||||
let target_os = self
|
||||
.target()
|
||||
.split('-')
|
||||
.nth(2)
|
||||
.unwrap_or(std::env::consts::OS);
|
||||
let target_os = self.target_platform();
|
||||
|
||||
let path = self.project_out_directory.join(binary.name());
|
||||
let mut path = self.project_out_directory.join(binary.name());
|
||||
|
||||
if target_os == "windows" {
|
||||
path.with_extension("exe")
|
||||
} else {
|
||||
path
|
||||
}
|
||||
if matches!(target_os, TargetPlatform::Windows) {
|
||||
// Append the `.exe` extension without overriding the existing extensions
|
||||
let extension = if let Some(extension) = path.extension() {
|
||||
let mut extension = extension.to_os_string();
|
||||
extension.push(".exe");
|
||||
extension
|
||||
} else {
|
||||
"exe".into()
|
||||
};
|
||||
path.set_extension(extension);
|
||||
};
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
/// Returns the list of binaries to bundle.
|
||||
@@ -932,18 +1075,13 @@ impl Settings {
|
||||
///
|
||||
/// Fails if the host/target's native package type is not supported.
|
||||
pub fn package_types(&self) -> crate::Result<Vec<PackageType>> {
|
||||
let target_os = self
|
||||
.target
|
||||
.split('-')
|
||||
.nth(2)
|
||||
.unwrap_or(std::env::consts::OS)
|
||||
.replace("darwin", "macos");
|
||||
let target_os = self.target_platform();
|
||||
|
||||
let platform_types = match target_os.as_str() {
|
||||
"macos" => vec![PackageType::MacOsBundle, PackageType::Dmg],
|
||||
"ios" => vec![PackageType::IosBundle],
|
||||
"linux" => vec![PackageType::Deb, PackageType::Rpm, PackageType::AppImage],
|
||||
"windows" => vec![PackageType::WindowsMsi, PackageType::Nsis],
|
||||
let platform_types = match target_os {
|
||||
TargetPlatform::MacOS => vec![PackageType::MacOsBundle, PackageType::Dmg],
|
||||
TargetPlatform::Ios => vec![PackageType::IosBundle],
|
||||
TargetPlatform::Linux => vec![PackageType::Deb, PackageType::Rpm, PackageType::AppImage],
|
||||
TargetPlatform::Windows => vec![PackageType::WindowsMsi, PackageType::Nsis],
|
||||
os => {
|
||||
return Err(crate::Error::GenericError(format!(
|
||||
"Native {os} bundles not yet supported."
|
||||
@@ -986,7 +1124,7 @@ impl Settings {
|
||||
|
||||
/// Returns an iterator over the icon files to be used for this bundle.
|
||||
pub fn icon_files(&self) -> ResourcePaths<'_> {
|
||||
match self.bundle_settings.icon {
|
||||
match self.icon_files {
|
||||
Some(ref paths) => ResourcePaths::new(paths.as_slice(), false),
|
||||
None => ResourcePaths::new(&[], false),
|
||||
}
|
||||
@@ -1030,7 +1168,7 @@ impl Settings {
|
||||
.to_string_lossy()
|
||||
.replace(&format!("-{}", self.target), ""),
|
||||
);
|
||||
common::copy_file(&src, &dest)?;
|
||||
fs_utils::copy_file(&src, &dest)?;
|
||||
paths.push(dest);
|
||||
}
|
||||
Ok(paths)
|
||||
@@ -1041,7 +1179,7 @@ impl Settings {
|
||||
for resource in self.resource_files().iter() {
|
||||
let resource = resource?;
|
||||
let dest = path.join(resource.target());
|
||||
common::copy_file(resource.path(), dest)?;
|
||||
fs_utils::copy_file(resource.path(), &dest)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1148,6 +1286,11 @@ impl Settings {
|
||||
&self.bundle_settings.dmg
|
||||
}
|
||||
|
||||
/// Returns the iOS settings.
|
||||
pub fn ios(&self) -> &IosSettings {
|
||||
&self.bundle_settings.ios
|
||||
}
|
||||
|
||||
/// Returns the MacOS settings.
|
||||
pub fn macos(&self) -> &MacOsSettings {
|
||||
&self.bundle_settings.macos
|
||||
@@ -1162,4 +1305,14 @@ impl Settings {
|
||||
pub fn updater(&self) -> Option<&UpdaterSettings> {
|
||||
self.bundle_settings.updater.as_ref()
|
||||
}
|
||||
|
||||
/// Whether to skip signing.
|
||||
pub fn no_sign(&self) -> bool {
|
||||
self.no_sign
|
||||
}
|
||||
|
||||
/// Set whether to skip signing.
|
||||
pub fn set_no_sign(&mut self, no_sign: bool) {
|
||||
self.no_sign = no_sign;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use super::common;
|
||||
|
||||
use crate::{
|
||||
bundle::{
|
||||
windows::{
|
||||
@@ -13,9 +11,11 @@ use crate::{
|
||||
},
|
||||
Bundle,
|
||||
},
|
||||
error::{Context, ErrorExt},
|
||||
utils::fs_utils,
|
||||
Settings,
|
||||
};
|
||||
use tauri_utils::display_path;
|
||||
use tauri_utils::{display_path, platform::Target as TargetPlatform};
|
||||
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
@@ -23,19 +23,13 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use zip::write::SimpleFileOptions;
|
||||
|
||||
// Build update
|
||||
pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<PathBuf>> {
|
||||
let target_os = settings
|
||||
.target()
|
||||
.split('-')
|
||||
.nth(2)
|
||||
.unwrap_or(std::env::consts::OS)
|
||||
.replace("darwin", "macos");
|
||||
let target_os = settings.target_platform();
|
||||
|
||||
if target_os == "windows" {
|
||||
if matches!(target_os, TargetPlatform::Windows) {
|
||||
return bundle_update_windows(settings, bundles);
|
||||
}
|
||||
|
||||
@@ -194,7 +188,7 @@ fn bundle_update_windows(settings: &Settings, bundles: &[Bundle]) -> crate::Resu
|
||||
p.push(c);
|
||||
(p, b)
|
||||
});
|
||||
let archived_path = archived_path.with_extension(format!("{}.zip", bundle_name));
|
||||
let archived_path = archived_path.with_extension(format!("{bundle_name}.zip"));
|
||||
|
||||
log::info!(action = "Bundling"; "{}", display_path(&archived_path));
|
||||
|
||||
@@ -210,7 +204,7 @@ fn bundle_update_windows(settings: &Settings, bundles: &[Bundle]) -> crate::Resu
|
||||
pub fn create_zip(src_file: &Path, dst_file: &Path) -> crate::Result<PathBuf> {
|
||||
let parent_dir = dst_file.parent().expect("No data in parent");
|
||||
fs::create_dir_all(parent_dir)?;
|
||||
let writer = common::create_file(dst_file)?;
|
||||
let writer = fs_utils::create_file(dst_file)?;
|
||||
|
||||
let file_name = src_file
|
||||
.file_name()
|
||||
@@ -222,7 +216,9 @@ pub fn create_zip(src_file: &Path, dst_file: &Path) -> crate::Result<PathBuf> {
|
||||
.unix_permissions(0o755);
|
||||
|
||||
zip.start_file(file_name.to_string_lossy(), options)?;
|
||||
let mut f = File::open(src_file)?;
|
||||
let mut f =
|
||||
File::open(src_file).fs_context("failed to open updater ZIP file", src_file.to_path_buf())?;
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
f.read_to_end(&mut buffer)?;
|
||||
zip.write_all(&buffer)?;
|
||||
@@ -235,7 +231,7 @@ pub fn create_zip(src_file: &Path, dst_file: &Path) -> crate::Result<PathBuf> {
|
||||
fn create_tar(src_dir: &Path, dest_path: &Path) -> crate::Result<PathBuf> {
|
||||
use flate2::{write::GzEncoder, Compression};
|
||||
|
||||
let dest_file = common::create_file(dest_path)?;
|
||||
let dest_file = fs_utils::create_file(dest_path)?;
|
||||
let gzip_encoder = GzEncoder::new(dest_file, Compression::default());
|
||||
|
||||
let gzip_encoder = create_tar_from_src(src_dir, gzip_encoder)?;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod msi;
|
||||
|
||||
pub mod nsis;
|
||||
pub mod sign;
|
||||
|
||||
|
||||
@@ -70,9 +70,16 @@
|
||||
<Property Id="ARPURLUPDATEINFO" Value="{{homepage}}"/>
|
||||
{{/if}}
|
||||
|
||||
<!-- initialize with previous InstallDir -->
|
||||
<!-- NOTE: The order of RegistrySearch elements below matters. In WIX, when multiple
|
||||
RegistrySearch elements are listed under a single Property, the LAST successful
|
||||
match wins. We list the NSIS default-key search first and the MSI InstallDir
|
||||
search second so that the MSI-specific path takes priority when both keys exist. -->
|
||||
<Property Id="INSTALLDIR">
|
||||
<RegistrySearch Id="PrevInstallDirReg" Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Name="InstallDir" Type="raw"/>
|
||||
<!-- First attempt: Search for the default key value (this is how the nsis installer stores the path) -->
|
||||
<RegistrySearch Id="PrevInstallDirNoName" Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Type="raw" />
|
||||
|
||||
<!-- Second attempt: Search for "InstallDir" which takes priority if found (this is how the msi installer stores the path) -->
|
||||
<RegistrySearch Id="PrevInstallDirWithName" Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Name="InstallDir" Type="raw" />
|
||||
</Property>
|
||||
|
||||
<!-- launch app checkbox -->
|
||||
@@ -276,38 +283,62 @@
|
||||
|
||||
{{#if install_webview}}
|
||||
<!-- WebView2 -->
|
||||
<Property Id="WVRTINSTALLED">
|
||||
<RegistrySearch Id="WVRTInstalledSystem" Root="HKLM" Key="SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Name="pv" Type="raw" Win64="no" />
|
||||
<RegistrySearch Id="WVRTInstalledUser" Root="HKCU" Key="SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Name="pv" Type="raw"/>
|
||||
<Property Id="INSTALLED_WEBVIEW2_VERSION">
|
||||
<RegistrySearch Id="Webview2VersionSystemx64" Root="HKLM" Key="SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Name="pv" Type="raw" />
|
||||
<RegistrySearch Id="Webview2VersionSystemx86" Root="HKLM" Key="SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Name="pv" Type="raw" />
|
||||
<RegistrySearch Id="Webview2VersionUser" Root="HKCU" Key="SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Name="pv" Type="raw"/>
|
||||
</Property>
|
||||
|
||||
{{#if download_bootstrapper}}
|
||||
<!-- Download webview bootstrapper mode -->
|
||||
<CustomAction Id='DownloadAndInvokeBootstrapper' Directory="INSTALLDIR" Execute="deferred" ExeCommand='powershell.exe -NoProfile -windowstyle hidden try [\{] [\[]Net.ServicePointManager[\]]::SecurityProtocol = [\[]Net.SecurityProtocolType[\]]::Tls12 [\}] catch [\{][\}]; Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/p/?LinkId=2124703" -OutFile "$env:TEMP\MicrosoftEdgeWebview2Setup.exe" ; Start-Process -FilePath "$env:TEMP\MicrosoftEdgeWebview2Setup.exe" -ArgumentList ({{webview_installer_args}} '/install') -Wait' Return='check'/>
|
||||
<InstallExecuteSequence>
|
||||
<Custom Action='DownloadAndInvokeBootstrapper' Before='InstallFinalize'>
|
||||
<![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>
|
||||
<![CDATA[NOT(REMOVE OR INSTALLED_WEBVIEW2_VERSION)]]>
|
||||
</Custom>
|
||||
</InstallExecuteSequence>
|
||||
{{/if}}
|
||||
|
||||
<!-- Embedded webview bootstrapper mode -->
|
||||
{{#if webview2_bootstrapper_path}}
|
||||
<!-- Embedded webview bootstrapper mode -->
|
||||
<Binary Id="MicrosoftEdgeWebview2Setup.exe" SourceFile="{{webview2_bootstrapper_path}}"/>
|
||||
<CustomAction Id='InvokeBootstrapper' BinaryKey='MicrosoftEdgeWebview2Setup.exe' Execute="deferred" ExeCommand='{{webview_installer_args}} /install' Return='check' />
|
||||
<InstallExecuteSequence>
|
||||
<Custom Action='InvokeBootstrapper' Before='InstallFinalize'>
|
||||
<![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>
|
||||
<![CDATA[NOT(REMOVE OR INSTALLED_WEBVIEW2_VERSION)]]>
|
||||
</Custom>
|
||||
</InstallExecuteSequence>
|
||||
{{/if}}
|
||||
|
||||
<!-- Embedded offline installer -->
|
||||
{{#if webview2_installer_path}}
|
||||
<!-- Embedded offline installer -->
|
||||
<Binary Id="MicrosoftEdgeWebView2RuntimeInstaller.exe" SourceFile="{{webview2_installer_path}}"/>
|
||||
<CustomAction Id='InvokeStandalone' BinaryKey='MicrosoftEdgeWebView2RuntimeInstaller.exe' Execute="deferred" ExeCommand='{{webview_installer_args}} /install' Return='check' />
|
||||
<InstallExecuteSequence>
|
||||
<Custom Action='InvokeStandalone' Before='InstallFinalize'>
|
||||
<![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>
|
||||
<![CDATA[NOT(REMOVE OR INSTALLED_WEBVIEW2_VERSION)]]>
|
||||
</Custom>
|
||||
</InstallExecuteSequence>
|
||||
{{/if}}
|
||||
|
||||
{{#if minimum_webview2_version}}
|
||||
<!-- Update WebView2 if minimum version requirement not met -->
|
||||
<Property Id="MINIMUM_WEBVIEW2_VERSION" Value="{{minimum_webview2_version}}" />
|
||||
<Property Id="EDGEUPDATE_PATH">
|
||||
<RegistrySearch Id="EdgeUpdateLocalMachine64" Root="HKLM" Key="SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate" Name="path" Type="raw" />
|
||||
<RegistrySearch Id="EdgeUpdateLocalMachine32" Root="HKLM" Key="SOFTWARE\Microsoft\EdgeUpdate" Name="path" Type="raw"/>
|
||||
<RegistrySearch Id="EdgeUpdateCurrentUser" Root="HKCU" Key="SOFTWARE\Microsoft\EdgeUpdate" Name="path" Type="raw"/>
|
||||
</Property>
|
||||
<!-- Chromium updater docs: https://source.chromium.org/chromium/chromium/src/+/main:docs/updater/user_manual.md -->
|
||||
<!-- Modified from "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Microsoft EdgeWebView\ModifyPath" -->
|
||||
<CustomAction Id="UpdateWebView2ViaEdgeUpdate" Execute="deferred" Property="EDGEUPDATE_PATH" Return="check" Impersonate="no" ExeCommand="/install appguid={F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}&needsadmin=true" />
|
||||
<InstallExecuteSequence>
|
||||
<Custom Action='UpdateWebView2ViaEdgeUpdate' Before='InstallFinalize'>
|
||||
<![CDATA[
|
||||
NOT REMOVE
|
||||
AND INSTALLED_WEBVIEW2_VERSION
|
||||
AND (INSTALLED_WEBVIEW2_VERSION < MINIMUM_WEBVIEW2_VERSION)
|
||||
]]>
|
||||
</Custom>
|
||||
</InstallExecuteSequence>
|
||||
{{/if}}
|
||||
|
||||
@@ -3,24 +3,30 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::bundle::{
|
||||
common::CommandExt,
|
||||
path_utils::{copy_file, FileOpts},
|
||||
settings::{Arch, Settings},
|
||||
windows::{
|
||||
sign::try_sign,
|
||||
util::{
|
||||
download_and_verify, download_webview2_bootstrapper, download_webview2_offline_installer,
|
||||
extract_zip, HashAlgorithm, WIX_OUTPUT_FOLDER_NAME, WIX_UPDATER_OUTPUT_FOLDER_NAME,
|
||||
use crate::{
|
||||
bundle::{
|
||||
settings::{Arch, Settings},
|
||||
windows::{
|
||||
sign::{should_sign, try_sign},
|
||||
util::{
|
||||
download_webview2_bootstrapper, download_webview2_offline_installer,
|
||||
WIX_OUTPUT_FOLDER_NAME, WIX_UPDATER_OUTPUT_FOLDER_NAME,
|
||||
},
|
||||
},
|
||||
},
|
||||
error::Context,
|
||||
utils::{
|
||||
fs_utils::copy_file,
|
||||
http_utils::{download_and_verify, extract_zip, HashAlgorithm},
|
||||
CommandExt,
|
||||
},
|
||||
};
|
||||
use anyhow::{bail, Context};
|
||||
use handlebars::{html_escape, to_json, Handlebars};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap, HashSet},
|
||||
ffi::OsStr,
|
||||
fs::{self, File},
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
@@ -171,7 +177,7 @@ impl ResourceDirectory {
|
||||
directories.push_str(wix_string.as_str());
|
||||
}
|
||||
let wix_string = if self.name.is_empty() {
|
||||
format!("{}{}", files, directories)
|
||||
format!("{files}{directories}")
|
||||
} else {
|
||||
format!(
|
||||
r#"<Directory Id="I{id}" Name="{name}">{files}{directories}</Directory>"#,
|
||||
@@ -197,14 +203,7 @@ fn copy_icon(settings: &Settings, filename: &str, path: &Path) -> crate::Result<
|
||||
|
||||
let icon_path = std::env::current_dir()?.join(path);
|
||||
|
||||
copy_file(
|
||||
icon_path,
|
||||
&icon_target_path,
|
||||
&FileOpts {
|
||||
overwrite: true,
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
copy_file(&icon_path, &icon_target_path)?;
|
||||
|
||||
Ok(icon_target_path)
|
||||
}
|
||||
@@ -222,8 +221,7 @@ fn app_installer_output_path(
|
||||
Arch::AArch64 => "arm64",
|
||||
target => {
|
||||
return Err(crate::Error::ArchError(format!(
|
||||
"Unsupported architecture: {:?}",
|
||||
target
|
||||
"Unsupported architecture: {target:?}"
|
||||
)))
|
||||
}
|
||||
};
|
||||
@@ -281,19 +279,40 @@ fn clear_env_for_wix(cmd: &mut Command) {
|
||||
}
|
||||
}
|
||||
|
||||
// WiX requires versions to be numeric only in a `major.minor.patch.build` format
|
||||
pub fn convert_version(version_str: &str) -> anyhow::Result<String> {
|
||||
let version = semver::Version::parse(version_str).context("invalid app version")?;
|
||||
if version.major > 255 {
|
||||
bail!("app version major number cannot be greater than 255");
|
||||
}
|
||||
if version.minor > 255 {
|
||||
bail!("app version minor number cannot be greater than 255");
|
||||
}
|
||||
if version.patch > 65535 {
|
||||
bail!("app version patch number cannot be greater than 65535");
|
||||
fn validate_wix_version(version_str: &str) -> crate::Result<()> {
|
||||
let components = version_str
|
||||
.split('.')
|
||||
.flat_map(|c| c.parse::<u64>().ok())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if components.len() < 3 {
|
||||
crate::error::bail!(
|
||||
"app wix version should be in the format major.minor.patch.build (build is optional)"
|
||||
);
|
||||
}
|
||||
|
||||
if components[0] > 255 {
|
||||
crate::error::bail!("app version major number cannot be greater than 255");
|
||||
}
|
||||
if components[1] > 255 {
|
||||
crate::error::bail!("app version minor number cannot be greater than 255");
|
||||
}
|
||||
if components[2] > 65535 {
|
||||
crate::error::bail!("app version patch number cannot be greater than 65535");
|
||||
}
|
||||
|
||||
if components.len() == 4 && components[3] > 65535 {
|
||||
crate::error::bail!("app version build number cannot be greater than 65535");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// WiX requires versions to be numeric only in a `major.minor.patch.build` format
|
||||
fn convert_version(version_str: &str) -> crate::Result<String> {
|
||||
let version = semver::Version::parse(version_str)
|
||||
.map_err(Into::into)
|
||||
.context("invalid app version")?;
|
||||
if !version.build.is_empty() {
|
||||
let build = version.build.parse::<u64>();
|
||||
if build.map(|b| b <= 65535).unwrap_or_default() {
|
||||
@@ -302,7 +321,7 @@ pub fn convert_version(version_str: &str) -> anyhow::Result<String> {
|
||||
version.major, version.minor, version.patch, version.build
|
||||
));
|
||||
} else {
|
||||
bail!("optional build metadata in app version must be numeric-only and cannot be greater than 65535 for msi target");
|
||||
crate::error::bail!("optional build metadata in app version must be numeric-only and cannot be greater than 65535 for msi target");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,7 +333,7 @@ pub fn convert_version(version_str: &str) -> anyhow::Result<String> {
|
||||
version.major, version.minor, version.patch, version.pre
|
||||
));
|
||||
} else {
|
||||
bail!("optional pre-release identifier in app version must be numeric-only and cannot be greater than 65535 for msi target");
|
||||
crate::error::bail!("optional pre-release identifier in app version must be numeric-only and cannot be greater than 65535 for msi target");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,8 +354,7 @@ fn run_candle(
|
||||
Arch::AArch64 => "arm64",
|
||||
target => {
|
||||
return Err(crate::Error::ArchError(format!(
|
||||
"unsupported architecture: {:?}",
|
||||
target
|
||||
"unsupported architecture: {target:?}"
|
||||
)))
|
||||
}
|
||||
};
|
||||
@@ -372,11 +390,7 @@ fn run_candle(
|
||||
cmd.arg(ext);
|
||||
}
|
||||
clear_env_for_wix(&mut cmd);
|
||||
cmd
|
||||
.args(&args)
|
||||
.current_dir(cwd)
|
||||
.output_ok()
|
||||
.context("error running candle.exe")?;
|
||||
cmd.args(&args).current_dir(cwd).output_ok()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -401,11 +415,7 @@ fn run_light(
|
||||
cmd.arg(ext);
|
||||
}
|
||||
clear_env_for_wix(&mut cmd);
|
||||
cmd
|
||||
.args(&args)
|
||||
.current_dir(build_path)
|
||||
.output_ok()
|
||||
.context("error running light.exe")?;
|
||||
cmd.args(&args).current_dir(build_path).output_ok()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -426,13 +436,23 @@ pub fn build_wix_app_installer(
|
||||
Arch::AArch64 => "arm64",
|
||||
target => {
|
||||
return Err(crate::Error::ArchError(format!(
|
||||
"unsupported architecture: {:?}",
|
||||
target
|
||||
"unsupported architecture: {target:?}"
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
let app_version = convert_version(settings.version_string())?;
|
||||
let app_version = if let Some(version) = settings
|
||||
.windows()
|
||||
.wix
|
||||
.as_ref()
|
||||
.and_then(|wix| wix.version.clone())
|
||||
{
|
||||
version
|
||||
} else {
|
||||
convert_version(settings.version_string())?
|
||||
};
|
||||
|
||||
validate_wix_version(&app_version)?;
|
||||
|
||||
// target only supports x64.
|
||||
log::info!("Target: {}", arch);
|
||||
@@ -444,6 +464,15 @@ pub fn build_wix_app_installer(
|
||||
}
|
||||
fs::create_dir_all(&output_path)?;
|
||||
|
||||
// when we're performing code signing, we'll sign some WiX DLLs, so we make a local copy
|
||||
let wix_toolset_path = if settings.windows().can_sign() {
|
||||
let wix_path = output_path.join("wix");
|
||||
crate::utils::fs_utils::copy_dir(wix_toolset_path, &wix_path)?;
|
||||
wix_path
|
||||
} else {
|
||||
wix_toolset_path.to_path_buf()
|
||||
};
|
||||
|
||||
let mut data = BTreeMap::new();
|
||||
|
||||
let silent_webview_install = if let WebviewInstallMode::DownloadBootstrapper { silent }
|
||||
@@ -503,6 +532,13 @@ pub fn build_wix_app_installer(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(minimum_webview2_version) = &settings.windows().minimum_webview2_version {
|
||||
data.insert(
|
||||
"minimum_webview2_version",
|
||||
to_json(minimum_webview2_version),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(license) = settings.license_file() {
|
||||
if license.ends_with(".rtf") {
|
||||
data.insert("license", to_json(license));
|
||||
@@ -604,7 +640,17 @@ pub fn build_wix_app_installer(
|
||||
data.insert("main_binary_path", to_json(main_binary_path));
|
||||
|
||||
// copy icon from `settings.windows().icon_path` folder to resource folder near msi
|
||||
let icon_path = copy_icon(settings, "icon.ico", &settings.windows().icon_path)?;
|
||||
#[allow(deprecated)]
|
||||
let icon_path = if !settings.windows().icon_path.as_os_str().is_empty() {
|
||||
settings.windows().icon_path.clone()
|
||||
} else {
|
||||
settings
|
||||
.icon_files()
|
||||
.flatten()
|
||||
.find(|i| i.extension() == Some(OsStr::new("ico")))
|
||||
.context("Couldn't find a .ico icon")?
|
||||
};
|
||||
let icon_path = copy_icon(settings, "icon.ico", &icon_path)?;
|
||||
|
||||
data.insert("icon_path", to_json(icon_path));
|
||||
|
||||
@@ -658,7 +704,9 @@ pub fn build_wix_app_installer(
|
||||
.iter()
|
||||
.flat_map(|p| &p.schemes)
|
||||
.collect::<Vec<_>>();
|
||||
data.insert("deep_link_protocols", to_json(schemes));
|
||||
if !schemes.is_empty() {
|
||||
data.insert("deep_link_protocols", to_json(schemes));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(path) = custom_template_path {
|
||||
@@ -686,58 +734,54 @@ pub fn build_wix_app_installer(
|
||||
);
|
||||
|
||||
// Create the update task XML
|
||||
let mut skip_uac_task = Handlebars::new();
|
||||
let skip_uac_task = Handlebars::new();
|
||||
let xml = include_str!("./update-task.xml");
|
||||
skip_uac_task
|
||||
.register_template_string("update.xml", xml)
|
||||
.map_err(|e| e.to_string())
|
||||
.expect("Failed to setup Update Task handlebars");
|
||||
let update_content = skip_uac_task.render_template(xml, &data)?;
|
||||
let temp_xml_path = output_path.join("update.xml");
|
||||
let update_content = skip_uac_task.render("update.xml", &data)?;
|
||||
fs::write(temp_xml_path, update_content)?;
|
||||
|
||||
// Create the Powershell script to install the task
|
||||
let mut skip_uac_task_installer = Handlebars::new();
|
||||
skip_uac_task_installer.register_escape_fn(handlebars::no_escape);
|
||||
let xml = include_str!("./install-task.ps1");
|
||||
skip_uac_task_installer
|
||||
.register_template_string("install-task.ps1", xml)
|
||||
.map_err(|e| e.to_string())
|
||||
.expect("Failed to setup Update Task Installer handlebars");
|
||||
let install_script_content = skip_uac_task_installer.render_template(xml, &data)?;
|
||||
let temp_ps1_path = output_path.join("install-task.ps1");
|
||||
let install_script_content = skip_uac_task_installer.render("install-task.ps1", &data)?;
|
||||
fs::write(temp_ps1_path, install_script_content)?;
|
||||
|
||||
// Create the Powershell script to uninstall the task
|
||||
let mut skip_uac_task_uninstaller = Handlebars::new();
|
||||
skip_uac_task_uninstaller.register_escape_fn(handlebars::no_escape);
|
||||
let xml = include_str!("./uninstall-task.ps1");
|
||||
skip_uac_task_uninstaller
|
||||
.register_template_string("uninstall-task.ps1", xml)
|
||||
.map_err(|e| e.to_string())
|
||||
.expect("Failed to setup Update Task Uninstaller handlebars");
|
||||
let install_script_content = skip_uac_task_uninstaller.render_template(xml, &data)?;
|
||||
let temp_ps1_path = output_path.join("uninstall-task.ps1");
|
||||
let install_script_content = skip_uac_task_uninstaller.render("uninstall-task.ps1", &data)?;
|
||||
fs::write(temp_ps1_path, install_script_content)?;
|
||||
|
||||
data.insert("enable_elevated_update_task", to_json(true));
|
||||
}
|
||||
|
||||
let main_wxs_path = output_path.join("main.wxs");
|
||||
fs::write(main_wxs_path, handlebars.render("main.wxs", &data)?)?;
|
||||
fs::write(&main_wxs_path, handlebars.render("main.wxs", &data)?)?;
|
||||
|
||||
let mut candle_inputs = vec![("main.wxs".into(), Vec::new())];
|
||||
let mut candle_inputs = vec![];
|
||||
|
||||
let current_dir = std::env::current_dir()?;
|
||||
let extension_regex = Regex::new("\"http://schemas.microsoft.com/wix/(\\w+)\"")?;
|
||||
for fragment_path in fragment_paths {
|
||||
let fragment_path = current_dir.join(fragment_path);
|
||||
let fragment = fs::read_to_string(&fragment_path)?;
|
||||
let input_paths =
|
||||
std::iter::once(main_wxs_path).chain(fragment_paths.iter().map(|p| current_dir.join(p)));
|
||||
|
||||
for input_path in input_paths {
|
||||
let input_content = fs::read_to_string(&input_path)?;
|
||||
let input_handlebars = Handlebars::new();
|
||||
let input = input_handlebars.render_template(&input_content, &data)?;
|
||||
let mut extensions = Vec::new();
|
||||
for cap in extension_regex.captures_iter(&fragment) {
|
||||
extensions.push(wix_toolset_path.join(format!("Wix{}.dll", &cap[1])));
|
||||
for cap in extension_regex.captures_iter(&input) {
|
||||
let path = wix_toolset_path.join(format!("Wix{}.dll", &cap[1]));
|
||||
if settings.windows().can_sign() {
|
||||
try_sign(&path, settings)?;
|
||||
}
|
||||
extensions.push(path);
|
||||
}
|
||||
candle_inputs.push((fragment_path, extensions));
|
||||
candle_inputs.push((input_path, extensions));
|
||||
}
|
||||
|
||||
let mut fragment_extensions = HashSet::new();
|
||||
@@ -745,11 +789,18 @@ pub fn build_wix_app_installer(
|
||||
fragment_extensions.insert(wix_toolset_path.join("WixUIExtension.dll"));
|
||||
fragment_extensions.insert(wix_toolset_path.join("WixUtilExtension.dll"));
|
||||
|
||||
// sign default extensions
|
||||
if settings.windows().can_sign() {
|
||||
for path in &fragment_extensions {
|
||||
try_sign(path, settings)?;
|
||||
}
|
||||
}
|
||||
|
||||
for (path, extensions) in candle_inputs {
|
||||
for ext in &extensions {
|
||||
fragment_extensions.insert(ext.clone());
|
||||
}
|
||||
run_candle(settings, wix_toolset_path, &output_path, path, extensions)?;
|
||||
run_candle(settings, &wix_toolset_path, &output_path, path, extensions)?;
|
||||
}
|
||||
|
||||
let mut output_paths = Vec::new();
|
||||
@@ -796,7 +847,7 @@ pub fn build_wix_app_installer(
|
||||
|
||||
let locale_contents = locale_contents.replace(
|
||||
"</WixLocalization>",
|
||||
&format!("{}</WixLocalization>", unset_locale_strings),
|
||||
&format!("{unset_locale_strings}</WixLocalization>"),
|
||||
);
|
||||
let locale_path = output_path.join("locale.wxl");
|
||||
{
|
||||
@@ -825,7 +876,7 @@ pub fn build_wix_app_installer(
|
||||
log::info!(action = "Running"; "light to produce {}", display_path(&msi_path));
|
||||
|
||||
run_light(
|
||||
wix_toolset_path,
|
||||
&wix_toolset_path,
|
||||
&output_path,
|
||||
arguments,
|
||||
&(fragment_extensions.clone().into_iter().collect()),
|
||||
@@ -833,7 +884,7 @@ pub fn build_wix_app_installer(
|
||||
)?;
|
||||
fs::rename(&msi_output_path, &msi_path)?;
|
||||
|
||||
if settings.can_sign() {
|
||||
if settings.windows().can_sign() {
|
||||
try_sign(&msi_path, settings)?;
|
||||
}
|
||||
|
||||
@@ -901,12 +952,11 @@ fn get_merge_modules(settings: &Settings) -> crate::Result<Vec<MergeModule>> {
|
||||
let mut merge_modules = Vec::new();
|
||||
let regex = Regex::new(r"[^\w\d\.]")?;
|
||||
for msm in glob::glob(
|
||||
settings
|
||||
.project_out_directory()
|
||||
.join("*.msm")
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
.as_str(),
|
||||
&PathBuf::from(glob::Pattern::escape(
|
||||
&settings.project_out_directory().to_string_lossy(),
|
||||
))
|
||||
.join("*.msm")
|
||||
.to_string_lossy(),
|
||||
)? {
|
||||
let path = msm?;
|
||||
let filename = path
|
||||
@@ -941,9 +991,12 @@ fn generate_resource_data(settings: &Settings) -> crate::Result<ResourceMap> {
|
||||
if added_resources.contains(&resource_path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
added_resources.push(resource_path.clone());
|
||||
|
||||
if settings.windows().can_sign() && should_sign(&resource_path)? {
|
||||
try_sign(&resource_path, settings)?;
|
||||
}
|
||||
|
||||
let resource_entry = ResourceFile {
|
||||
id: format!("I{}", Uuid::new_v4().as_simple()),
|
||||
guid: Uuid::new_v4().to_string(),
|
||||
@@ -1013,8 +1066,13 @@ fn generate_resource_data(settings: &Settings) -> crate::Result<ResourceMap> {
|
||||
|
||||
let mut dlls = Vec::new();
|
||||
|
||||
// TODO: The bundler should not include all DLLs it finds. Instead it should only include WebView2Loader.dll if present and leave the rest to the resources config.
|
||||
let out_dir = settings.project_out_directory();
|
||||
for dll in glob::glob(out_dir.join("*.dll").to_string_lossy().to_string().as_str())? {
|
||||
for dll in glob::glob(
|
||||
&PathBuf::from(glob::Pattern::escape(&out_dir.to_string_lossy()))
|
||||
.join("*.dll")
|
||||
.to_string_lossy(),
|
||||
)? {
|
||||
let path = dll?;
|
||||
let resource_path = dunce::simplified(&path);
|
||||
let relative_path = path
|
||||
@@ -1023,6 +1081,10 @@ fn generate_resource_data(settings: &Settings) -> crate::Result<ResourceMap> {
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
if !added_resources.iter().any(|r| r.ends_with(&relative_path)) {
|
||||
if settings.windows().can_sign() {
|
||||
try_sign(resource_path, settings)?;
|
||||
}
|
||||
|
||||
dlls.push(ResourceFile {
|
||||
id: format!("I{}", Uuid::new_v4().as_simple()),
|
||||
guid: Uuid::new_v4().to_string(),
|
||||
@@ -1032,16 +1094,48 @@ fn generate_resource_data(settings: &Settings) -> crate::Result<ResourceMap> {
|
||||
}
|
||||
|
||||
if !dlls.is_empty() {
|
||||
resources.insert(
|
||||
"".to_string(),
|
||||
ResourceDirectory {
|
||||
resources
|
||||
.entry("".to_string())
|
||||
.and_modify(|r| r.files.append(&mut dlls))
|
||||
.or_insert(ResourceDirectory {
|
||||
path: "".to_string(),
|
||||
name: "".to_string(),
|
||||
directories: vec![],
|
||||
files: dlls,
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Ok(resources)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn validates_wix_version() {
|
||||
assert!(validate_wix_version("1.1.1").is_ok());
|
||||
assert!(validate_wix_version("1.1.1.1").is_ok());
|
||||
assert!(validate_wix_version("255.1.1.1").is_ok());
|
||||
assert!(validate_wix_version("1.255.1.1").is_ok());
|
||||
assert!(validate_wix_version("1.1.65535.1").is_ok());
|
||||
assert!(validate_wix_version("1.1.1.65535").is_ok());
|
||||
|
||||
assert!(validate_wix_version("256.1.1.1").is_err());
|
||||
assert!(validate_wix_version("1.256.1.1").is_err());
|
||||
assert!(validate_wix_version("1.1.65536.1").is_err());
|
||||
assert!(validate_wix_version("1.1.1.65536").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_version_to_wix() {
|
||||
assert_eq!(convert_version("1.1.2").unwrap(), "1.1.2");
|
||||
assert_eq!(convert_version("1.1.2-4").unwrap(), "1.1.2.4");
|
||||
assert_eq!(convert_version("1.1.2-65535").unwrap(), "1.1.2.65535");
|
||||
assert_eq!(convert_version("1.1.2+2").unwrap(), "1.1.2.2");
|
||||
|
||||
assert!(convert_version("1.1.2-alpha").is_err());
|
||||
assert!(convert_version("1.1.2-alpha.4").is_err());
|
||||
assert!(convert_version("1.1.2+asd.3").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<Actions Context="Author">
|
||||
<Exec>
|
||||
<Command>cmd.exe</Command>
|
||||
<Arguments>/c "%SYSTEMROOT%\System32\msiexec.exe /i %TEMP%\\{{product_name}}.msi {{msiexec_args}} /promptrestart"</Arguments>
|
||||
<Arguments>/c ^"%SYSTEMROOT%\System32\msiexec.exe /i "%TEMP%\\{{product_name}}.msi" {{msiexec_args}} /promptrestart^"</Arguments>
|
||||
</Exec>
|
||||
</Actions>
|
||||
</Task>
|
||||
|
||||
@@ -41,13 +41,15 @@ ${StrLoc}
|
||||
!define INSTALLERICON "{{installer_icon}}"
|
||||
!define SIDEBARIMAGE "{{sidebar_image}}"
|
||||
!define HEADERIMAGE "{{header_image}}"
|
||||
!define UNINSTALLERICON "{{uninstaller_icon}}"
|
||||
!define UNINSTALLERHEADERIMAGE "{{uninstaller_header_image}}"
|
||||
!define MAINBINARYNAME "{{main_binary_name}}"
|
||||
!define MAINBINARYSRCPATH "{{main_binary_path}}"
|
||||
!define BUNDLEID "{{bundle_id}}"
|
||||
!define COPYRIGHT "{{copyright}}"
|
||||
!define OUTFILE "{{out_file}}"
|
||||
!define ARCH "{{arch}}"
|
||||
!define PLUGINSPATH "{{additional_plugins_path}}"
|
||||
!define ADDITIONALPLUGINSPATH "{{additional_plugins_path}}"
|
||||
!define ALLOWDOWNGRADES "{{allow_downgrades}}"
|
||||
!define DISPLAYLANGUAGESELECTOR "{{display_language_selector}}"
|
||||
!define INSTALLWEBVIEW2MODE "{{install_webview2_mode}}"
|
||||
@@ -56,7 +58,8 @@ ${StrLoc}
|
||||
!define WEBVIEW2INSTALLERPATH "{{webview2_installer_path}}"
|
||||
!define MINIMUMWEBVIEW2VERSION "{{minimum_webview2_version}}"
|
||||
!define UNINSTKEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCTNAME}"
|
||||
!define MANUPRODUCTKEY "Software\${MANUFACTURER}\${PRODUCTNAME}"
|
||||
!define MANUKEY "Software\${MANUFACTURER}"
|
||||
!define MANUPRODUCTKEY "${MANUKEY}\${PRODUCTNAME}"
|
||||
!define UNINSTALLERSIGNCOMMAND "{{uninstaller_sign_cmd}}"
|
||||
!define ESTIMATEDSIZE "{{estimated_size}}"
|
||||
!define STARTMENUFOLDER "{{start_menu_folder}}"
|
||||
@@ -84,10 +87,8 @@ VIAddVersionKey "LegalCopyright" "${COPYRIGHT}"
|
||||
VIAddVersionKey "FileVersion" "${VERSION}"
|
||||
VIAddVersionKey "ProductVersion" "${VERSION}"
|
||||
|
||||
; Plugins path, currently exists for linux only
|
||||
!if "${PLUGINSPATH}" != ""
|
||||
!addplugindir "${PLUGINSPATH}"
|
||||
!endif
|
||||
# additional plugins
|
||||
!addplugindir "${ADDITIONALPLUGINSPATH}"
|
||||
|
||||
; Uninstaller signing command
|
||||
!if "${UNINSTALLERSIGNCOMMAND}" != ""
|
||||
@@ -96,7 +97,7 @@ VIAddVersionKey "ProductVersion" "${VERSION}"
|
||||
|
||||
; Handle install mode, `perUser`, `perMachine` or `both`
|
||||
!if "${INSTALLMODE}" == "perMachine"
|
||||
RequestExecutionLevel highest
|
||||
RequestExecutionLevel admin
|
||||
!endif
|
||||
|
||||
!if "${INSTALLMODE}" == "currentUser"
|
||||
@@ -130,10 +131,26 @@ VIAddVersionKey "ProductVersion" "${VERSION}"
|
||||
!define MUI_WELCOMEFINISHPAGE_BITMAP "${SIDEBARIMAGE}"
|
||||
!endif
|
||||
|
||||
; Installer header image
|
||||
; Enable header images for installer and uninstaller pages when either image is configured.
|
||||
!if "${HEADERIMAGE}" != ""
|
||||
!define MUI_HEADERIMAGE
|
||||
!define MUI_HEADERIMAGE_BITMAP "${HEADERIMAGE}"
|
||||
!else if "${UNINSTALLERHEADERIMAGE}" != ""
|
||||
!define MUI_HEADERIMAGE
|
||||
!endif
|
||||
|
||||
; Installer header image
|
||||
!if "${HEADERIMAGE}" != ""
|
||||
!define MUI_HEADERIMAGE_BITMAP "${HEADERIMAGE}"
|
||||
!endif
|
||||
|
||||
; Uninstaller header image
|
||||
!if "${UNINSTALLERHEADERIMAGE}" != ""
|
||||
!define MUI_HEADERIMAGE_UNBITMAP "${UNINSTALLERHEADERIMAGE}"
|
||||
!endif
|
||||
|
||||
; Uninstaller icon
|
||||
!if "${UNINSTALLERICON}" != ""
|
||||
!define MUI_UNICON "${UNINSTALLERICON}"
|
||||
!endif
|
||||
|
||||
; Define registry key to store installer language
|
||||
@@ -619,7 +636,7 @@ Section Install
|
||||
!insertmacro NSIS_HOOK_PREINSTALL
|
||||
!endif
|
||||
|
||||
!insertmacro CheckIfAppIsRunning
|
||||
!insertmacro CheckIfAppIsRunning "${MAINBINARYNAME}.exe" "${PRODUCTNAME}"
|
||||
|
||||
; Copy main executable
|
||||
File "${MAINBINARYSRCPATH}"
|
||||
@@ -629,12 +646,12 @@ Section Install
|
||||
CreateDirectory "$INSTDIR\\{{this}}"
|
||||
{{/each}}
|
||||
{{#each resources}}
|
||||
File /a "/oname={{this.[1]}}" "{{@key}}"
|
||||
File /a "/oname={{this.[1]}}" "{{no-escape @key}}"
|
||||
{{/each}}
|
||||
|
||||
; Copy external binaries
|
||||
{{#each binaries}}
|
||||
File /a "/oname={{this}}" "{{@key}}"
|
||||
File /a "/oname={{this}}" "{{no-escape @key}}"
|
||||
{{/each}}
|
||||
|
||||
; Create file associations
|
||||
@@ -756,7 +773,7 @@ Section Uninstall
|
||||
!insertmacro NSIS_HOOK_PREUNINSTALL
|
||||
!endif
|
||||
|
||||
!insertmacro CheckIfAppIsRunning
|
||||
!insertmacro CheckIfAppIsRunning "${MAINBINARYNAME}.exe" "${PRODUCTNAME}"
|
||||
|
||||
; Delete the app directory and its content from disk
|
||||
; Copy main executable
|
||||
@@ -834,12 +851,27 @@ Section Uninstall
|
||||
DeleteRegKey HKCU "${UNINSTKEY}"
|
||||
!endif
|
||||
|
||||
DeleteRegValue HKCU "${MANUPRODUCTKEY}" "Installer Language"
|
||||
; Removes the Autostart entry for ${PRODUCTNAME} from the HKCU Run key if it exists.
|
||||
; This ensures the program does not launch automatically after uninstallation if it exists.
|
||||
; If it doesn't exist, it does nothing.
|
||||
; We do this when not updating (to preserve the registry value on updates)
|
||||
${If} $UpdateMode <> 1
|
||||
DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\Run" "${PRODUCTNAME}"
|
||||
${EndIf}
|
||||
|
||||
; Delete app data if the checkbox is selected
|
||||
; and if not updating
|
||||
${If} $DeleteAppDataCheckboxState = 1
|
||||
${AndIf} $UpdateMode <> 1
|
||||
; Clear the install location $INSTDIR from registry
|
||||
DeleteRegKey SHCTX "${MANUPRODUCTKEY}"
|
||||
DeleteRegKey /ifempty SHCTX "${MANUKEY}"
|
||||
|
||||
; Clear the install language from registry
|
||||
DeleteRegValue HKCU "${MANUPRODUCTKEY}" "Installer Language"
|
||||
DeleteRegKey /ifempty HKCU "${MANUPRODUCTKEY}"
|
||||
DeleteRegKey /ifempty HKCU "${MANUKEY}"
|
||||
|
||||
SetShellVarContext current
|
||||
RmDir /r "$APPDATA\${BUNDLEID}"
|
||||
RmDir /r "$LOCALAPPDATA\${BUNDLEID}"
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
LangString addOrReinstall ${LANG_ARABIC} "إضافة أو إزالة المكونات"
|
||||
LangString alreadyInstalled ${LANG_ARABIC} "التطبيق مثبت بالفعل"
|
||||
LangString alreadyInstalledLong ${LANG_ARABIC} "${PRODUCTNAME} ${VERSION} مثبت بالفعل. قم باختيار العملية التى تريدها ثم اضغط على التالى."
|
||||
LangString appRunning ${LANG_ARABIC} "${PRODUCTNAME} مازال يعمل! من فضلك، قم بإغلاق التطبيق أولاً ثم حاول مرة أخرى."
|
||||
LangString appRunningOkKill ${LANG_ARABIC} "${PRODUCTNAME} مازال يعمل!$\nاضغط OK لإغلاقه"
|
||||
LangString appRunning ${LANG_ARABIC} "{{product_name}} مازال يعمل! من فضلك، قم بإغلاق التطبيق أولاً ثم حاول مرة أخرى."
|
||||
LangString appRunningOkKill ${LANG_ARABIC} "{{product_name}} مازال يعمل!$\nاضغط OK لإغلاقه"
|
||||
LangString chooseMaintenanceOption ${LANG_ARABIC} "قم باختيار نوع الصيانة التى تريدها."
|
||||
LangString choowHowToInstall ${LANG_ARABIC} "قم باختيار طريقة تنصيب ${PRODUCTNAME}."
|
||||
LangString createDesktop ${LANG_ARABIC} "اضف اختصار على سطح المكتب"
|
||||
LangString dontUninstall ${LANG_ARABIC} "عدم إزالة"
|
||||
LangString dontUninstallDowngrade ${LANG_ARABIC} "عدم إزالة (التخفيض بدون إزالة غير مسموح لهذا المثبت)"
|
||||
LangString failedToKillApp ${LANG_ARABIC} "فشل فى غلف ${PRODUCTNAME}. من فضلك، قم بإغلاق التطبيق أولاً ثم حاول مرة أخرى."
|
||||
LangString failedToKillApp ${LANG_ARABIC} "فشل فى غلف {{product_name}}. من فضلك، قم بإغلاق التطبيق أولاً ثم حاول مرة أخرى."
|
||||
LangString installingWebview2 ${LANG_ARABIC} "تنصيب WebView2..."
|
||||
LangString newerVersionInstalled ${LANG_ARABIC} "يوجد نسخة جديدة من ${PRODUCTNAME} مثبتة بالغعل! لا ينصح بتنصيب نسخة اقدم من النسخة الحالية. اذا مازلت ترغب فى تنصيب النسخة الأقدم، فينصح بإزالة النسخة الحالية أولاً. قم باختيار العملية التى تريدها ثم اضغط على التالى للاستمرار."
|
||||
LangString older ${LANG_ARABIC} "أقدم"
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
LangString addOrReinstall ${LANG_BULGARIAN} "Добавяне/Преинсталиране на компоненти"
|
||||
LangString alreadyInstalled ${LANG_BULGARIAN} "Вече инсталиран"
|
||||
LangString alreadyInstalledLong ${LANG_BULGARIAN} "${PRODUCTNAME} ${VERSION} е вече е инсталиран. Изберете операцията, която искате да извършите и натиснете Напред, за да продължите."
|
||||
LangString appRunning ${LANG_BULGARIAN} "${PRODUCTNAME} е отворен! Моля, затворете го първо и опитайте отново."
|
||||
LangString appRunningOkKill ${LANG_BULGARIAN} "${PRODUCTNAME} е отворен!$\nНатиснете ОК, за да го затворите."
|
||||
LangString appRunning ${LANG_BULGARIAN} "{{product_name}} е отворен! Моля, затворете го първо и опитайте отново."
|
||||
LangString appRunningOkKill ${LANG_BULGARIAN} "{{product_name}} е отворен!$\nНатиснете ОК, за да го затворите."
|
||||
LangString chooseMaintenanceOption ${LANG_BULGARIAN} "Изберете опция за поддръжка."
|
||||
LangString choowHowToInstall ${LANG_BULGARIAN} "Изберете как искате да инсталирате ${PRODUCTNAME}."
|
||||
LangString createDesktop ${LANG_BULGARIAN} "Създайте пряк път на работния плот"
|
||||
LangString dontUninstall ${LANG_BULGARIAN} "Не деинсталирайте"
|
||||
LangString dontUninstallDowngrade ${LANG_BULGARIAN} "Не деинсталирайте (Понижаването без деинсталация е забранено за този инсталатор)"
|
||||
LangString failedToKillApp ${LANG_BULGARIAN} "Неуспешно прекратяване на ${PRODUCTNAME}. Моля, затворете го първо и опитайте отново."
|
||||
LangString failedToKillApp ${LANG_BULGARIAN} "Неуспешно прекратяване на {{product_name}}. Моля, затворете го първо и опитайте отново."
|
||||
LangString installingWebview2 ${LANG_BULGARIAN} "Инсталиране на WebView2..."
|
||||
LangString newerVersionInstalled ${LANG_BULGARIAN} "Вече е инсталирана по-нова версия на ${PRODUCTNAME}! Не се препоръчва да инсталирате по-стара версия. Ако наистина желаете да инсталирате тази по-стара версия, по-добре е да деинсталирате текущата версия първо. Изберете операцията, която искате да извършите и натиснете Напред, за да продължите."
|
||||
LangString older ${LANG_BULGARIAN} "по-стара"
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
LangString addOrReinstall ${LANG_DUTCH} "(Her)installeer componenten"
|
||||
LangString alreadyInstalled ${LANG_DUTCH} "Al geïnstalleerd"
|
||||
LangString alreadyInstalledLong ${LANG_DUTCH} "${PRODUCTNAME} ${VERSION} is al geïnstalleerd. Kies een van de volgende opties en klik op Volgende om door te gaan."
|
||||
LangString appRunning ${LANG_DUTCH} "${PRODUCTNAME} is geopend! Sluit het programma eerst en probeer het dan opnieuw."
|
||||
LangString appRunningOkKill ${LANG_DUTCH} "${PRODUCTNAME} is geopend!$\nKlik op OK om het te stoppen."
|
||||
LangString appRunning ${LANG_DUTCH} "{{product_name}} is geopend! Sluit het programma eerst en probeer het dan opnieuw."
|
||||
LangString appRunningOkKill ${LANG_DUTCH} "{{product_name}} is geopend!$\nKlik op OK om het te stoppen."
|
||||
LangString chooseMaintenanceOption ${LANG_DUTCH} "Kies de onderhoudsoptie die u wilt uitvoeren."
|
||||
LangString choowHowToInstall ${LANG_DUTCH} "Kies hoe u ${PRODUCTNAME} wilt installeren."
|
||||
LangString createDesktop ${LANG_DUTCH} "Maak een snelkoppeling aan op het bureaublad"
|
||||
LangString dontUninstall ${LANG_DUTCH} "Deïnstalleer niet"
|
||||
LangString dontUninstallDowngrade ${LANG_DUTCH} "Deïnstalleer niet (Downgraden zonder deïnstalleren is uitgeschakeld voor deze installer)"
|
||||
LangString failedToKillApp ${LANG_DUTCH} "Het is niet gelukt ${PRODUCTNAME} te stoppen. Sluit het eerst zelf en probeer het dan nog een keer"
|
||||
LangString failedToKillApp ${LANG_DUTCH} "Het is niet gelukt {{product_name}} te stoppen. Sluit het eerst zelf en probeer het dan nog een keer"
|
||||
LangString installingWebview2 ${LANG_DUTCH} "WebView2 wordt geïnstalleerd..."
|
||||
LangString newerVersionInstalled ${LANG_DUTCH} "Een nieuwere versie van ${PRODUCTNAME} is al geïnstalleerd! Het word niet aangeraden om een oudere versie te installeren. Als u echt deze oudere versie wilt installeren, kunt u beter de huidige versie eerst deïnstalleren. Kies een van de volgende opties en klik op Volgende om door te gaan."
|
||||
LangString older ${LANG_DUTCH} "oudere"
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
LangString addOrReinstall ${LANG_ENGLISH} "Add/Reinstall components"
|
||||
LangString alreadyInstalled ${LANG_ENGLISH} "Already Installed"
|
||||
LangString alreadyInstalledLong ${LANG_ENGLISH} "${PRODUCTNAME} ${VERSION} is already installed. Select the operation you want to perform and click Next to continue."
|
||||
LangString appRunning ${LANG_ENGLISH} "${PRODUCTNAME} is running! Please close it first then try again."
|
||||
LangString appRunningOkKill ${LANG_ENGLISH} "${PRODUCTNAME} is running!$\nClick OK to kill it"
|
||||
LangString appRunning ${LANG_ENGLISH} "{{product_name}} is running! Please close it first then try again."
|
||||
LangString appRunningOkKill ${LANG_ENGLISH} "{{product_name}} is running!$\nClick OK to kill it"
|
||||
LangString chooseMaintenanceOption ${LANG_ENGLISH} "Choose the maintenance option to perform."
|
||||
LangString choowHowToInstall ${LANG_ENGLISH} "Choose how you want to install ${PRODUCTNAME}."
|
||||
LangString createDesktop ${LANG_ENGLISH} "Create desktop shortcut"
|
||||
LangString dontUninstall ${LANG_ENGLISH} "Do not uninstall"
|
||||
LangString dontUninstallDowngrade ${LANG_ENGLISH} "Do not uninstall (Downgrading without uninstall is disabled for this installer)"
|
||||
LangString failedToKillApp ${LANG_ENGLISH} "Failed to kill ${PRODUCTNAME}. Please close it first then try again"
|
||||
LangString failedToKillApp ${LANG_ENGLISH} "Failed to kill {{product_name}}. Please close it first then try again"
|
||||
LangString installingWebview2 ${LANG_ENGLISH} "Installing WebView2..."
|
||||
LangString newerVersionInstalled ${LANG_ENGLISH} "A newer version of ${PRODUCTNAME} is already installed! It is not recommended that you install an older version. If you really want to install this older version, it's better to uninstall the current version first. Select the operation you want to perform and click Next to continue."
|
||||
LangString older ${LANG_ENGLISH} "older"
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
LangString addOrReinstall ${LANG_FRENCH} "Ajouter/Réinstaller un composant."
|
||||
LangString alreadyInstalled ${LANG_FRENCH} "Déja installé."
|
||||
LangString alreadyInstalledLong ${LANG_FRENCH} "${PRODUCTNAME} ${VERSION} est déja installé. Sélectionnez l'opération que vous souhaitez effectuer, puis cliquez sur Suivant pour continuer."
|
||||
LangString appRunning ${LANG_FRENCH} "${PRODUCTNAME} est en cours d'exécution. Veuillez fermer l'application avant de réessayer."
|
||||
LangString appRunningOkKill ${LANG_FRENCH} "${PRODUCTNAME} est en cours d'exécution.$\nCliquez sur OK pour fermer l'application."
|
||||
LangString appRunning ${LANG_FRENCH} "{{product_name}} est en cours d'exécution. Veuillez fermer l'application avant de réessayer."
|
||||
LangString appRunningOkKill ${LANG_FRENCH} "{{product_name}} est en cours d'exécution.$\nCliquez sur OK pour fermer l'application."
|
||||
LangString chooseMaintenanceOption ${LANG_FRENCH} "Veuillez choisir l'option de maintenance à effectuer."
|
||||
LangString choowHowToInstall ${LANG_FRENCH} "Veuillez choisir l'emplacement d'installation de ${PRODUCTNAME}."
|
||||
LangString createDesktop ${LANG_FRENCH} "Créer un raccourci sur le bureau."
|
||||
LangString dontUninstall ${LANG_FRENCH} "Ne pas désinstaller"
|
||||
LangString dontUninstallDowngrade ${LANG_FRENCH} "Ne pas désinstaller (revenir à une ancienne version sans désinstallation est désactivé pour cet installateur)"
|
||||
LangString failedToKillApp ${LANG_FRENCH} "La fermeture de ${PRODUCTNAME} a échoué. Veuillez fermer l'application et réessayer."
|
||||
LangString failedToKillApp ${LANG_FRENCH} "La fermeture de {{product_name}} a échoué. Veuillez fermer l'application et réessayer."
|
||||
LangString installingWebview2 ${LANG_FRENCH} "Installation de WebView2..."
|
||||
LangString newerVersionInstalled ${LANG_FRENCH} "Une version plus récente de ${PRODUCTNAME} est déja installée. Il n'est pas recommandé d'installer une ancienne version. Si vous souhaitez installer cette ancienne version, il est conseillé de désinstaller la version courante en premier. Veuillez sélectionner l'opération que vous souhaitez effectuer, puis cliquez sur Suivant pour continer."
|
||||
LangString older ${LANG_FRENCH} "ancien"
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
LangString addOrReinstall ${LANG_GERMAN} "Komponenten hinzufügen/neu installieren"
|
||||
LangString alreadyInstalled ${LANG_GERMAN} "Bereits installiert"
|
||||
LangString alreadyInstalledLong ${LANG_GERMAN} "${PRODUCTNAME} ${VERSION} ist bereits installiert. Wählen Sie den gewünschten Vorgang aus und klicken Sie auf Weiter, um fortzufahren."
|
||||
LangString appRunning ${LANG_GERMAN} "${PRODUCTNAME} wird ausgeführt! Bitte schließen Sie es zuerst und versuchen Sie es dann erneut."
|
||||
LangString appRunningOkKill ${LANG_GERMAN} "${PRODUCTNAME} läuft! $\nKlicken Sie auf OK, um es zu beenden"
|
||||
LangString appRunning ${LANG_GERMAN} "{{product_name}} wird ausgeführt! Bitte schließen Sie es zuerst und versuchen Sie es dann erneut."
|
||||
LangString appRunningOkKill ${LANG_GERMAN} "{{product_name}} läuft! $\nKlicken Sie auf OK, um es zu beenden"
|
||||
LangString chooseMaintenanceOption ${LANG_GERMAN} "Wählen Sie die auszuführende Wartungsoption."
|
||||
LangString choowHowToInstall ${LANG_GERMAN} "Wählen Sie, wie Sie ${PRODUCTNAME} installieren möchten."
|
||||
LangString createDesktop ${LANG_GERMAN} "Desktop-Verknüpfung erstellen"
|
||||
LangString dontUninstall ${LANG_GERMAN} "Nicht deinstallieren"
|
||||
LangString dontUninstallDowngrade ${LANG_GERMAN} "Nicht deinstallieren (Downgrading ohne Deinstallation ist für dieses Installationsprogramm deaktiviert)"
|
||||
LangString failedToKillApp ${LANG_GERMAN} "Failed to kill ${PRODUCTNAME}. Bitte schließen Sie es zuerst und versuchen Sie es dann erneut"
|
||||
LangString failedToKillApp ${LANG_GERMAN} "Failed to kill {{product_name}}. Bitte schließen Sie es zuerst und versuchen Sie es dann erneut"
|
||||
LangString installingWebview2 ${LANG_GERMAN} "Installiere WebView2..."
|
||||
LangString newerVersionInstalled ${LANG_GERMAN} "Eine neuere Version von ${PRODUCTNAME} ist bereits installiert! Es wird nicht empfohlen, eine ältere Version zu installieren. Wenn Sie diese ältere Version wirklich installieren wollen, ist es besser, die aktuelle Version zuerst zu deinstallieren. Wählen Sie den gewünschten Vorgang aus und klicken Sie auf Weiter, um fortzufahren."
|
||||
LangString älter ${LANG_GERMAN} "älter"
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
LangString addOrReinstall ${LANG_HEBREW} "הוסף או התקן מחדש"
|
||||
LangString alreadyInstalled ${LANG_HEBREW} "כבר מותקן"
|
||||
LangString alreadyInstalledLong ${LANG_HEBREW} "${PRODUCTNAME} ${VERSION} כבר מותקן. בחר את הפעולה שברצונך לבצע ולחץ על הבא כדי להמשיך."
|
||||
LangString appRunning ${LANG_HEBREW} "{{product_name}} פועל! נא לסגור אותו ולנסות שוב."
|
||||
LangString appRunningOkKill ${LANG_HEBREW} "{{product_name}} פועל!$\nלחץ אישור כדי לסגור אותו."
|
||||
LangString chooseMaintenanceOption ${LANG_HEBREW} "בחר את פעולת התחזוקה לביצוע"
|
||||
LangString choowHowToInstall ${LANG_HEBREW} "בחר איך תרצה להתקין את ${PRODUCTNAME}."
|
||||
LangString createDesktop ${LANG_HEBREW} "צור קיצור דרך בשולחן העבודה"
|
||||
LangString dontUninstall ${LANG_HEBREW} "אל תסיר"
|
||||
LangString dontUninstallDowngrade ${LANG_HEBREW} "אל תסיר (התקנת גרסה ישנה ללא הסרת הגרסה הנוכחית מושעית עבור התקנה זו)"
|
||||
LangString failedToKillApp ${LANG_HEBREW} "עצירת {{product_name}} נכשלה. נא לסגור את היישום ולנסות שוב."
|
||||
LangString installingWebview2 ${LANG_HEBREW} "מתקין את WebView2..."
|
||||
LangString newerVersionInstalled ${LANG_HEBREW} "גרסה חדשה יותר של ${PRODUCTNAME} כבר מותקנת! לא מומלץ להתקין גרסה ישנה. אם בכל זאת תרצה להתקין את הגרסה הזו, מומלץ קודם להסיר את הגרסה הנוכחית. בחר את הפעולה שברצונך לבצע ולחץ הבא להמשך."
|
||||
LangString older ${LANG_HEBREW} "ישנה"
|
||||
LangString olderOrUnknownVersionInstalled ${LANG_HEBREW} "גרסה $R4 של ${PRODUCTNAME} מותקנת במערכת שלך. מומלץ להסיר את הגרסה הנוכחית לפני ההתקנה. בחר את הפעולה שברצונך לבצע ולחץ הבא להמשך."
|
||||
LangString silentDowngrades ${LANG_HEBREW} "התקנת גרסה ישנה לא נתמכת בהתקנה זו, אין אפשרות להמשיך עם ההתקנה השקטה, נא להמשיך עם ההתקנה בממשק הגרפי.$\n"
|
||||
LangString unableToUninstall ${LANG_HEBREW} "לא ניתן להסיר!"
|
||||
LangString uninstallApp ${LANG_HEBREW} "הסר את ${PRODUCTNAME}"
|
||||
LangString uninstallBeforeInstalling ${LANG_HEBREW} "הסר את הגרסה הנוכחית לפני התקנת גרסה זו"
|
||||
LangString unknown ${LANG_HEBREW} "לא ידועה"
|
||||
LangString webview2AbortError ${LANG_HEBREW} "התקנת WebView2 נכשלה! היישום אינו יכולה לפעול בלי זה. נסה להפעיל את ההתקנה שוב."
|
||||
LangString webview2DownloadError ${LANG_HEBREW} "שגיאה: הורדת WebView2 נכשלה - $0"
|
||||
LangString webview2DownloadSuccess ${LANG_HEBREW} "מאתחל WebView2 הורד בהצלחה"
|
||||
LangString webview2Downloading ${LANG_HEBREW} "מוריד את מאתחל WebView2..."
|
||||
LangString webview2InstallError ${LANG_HEBREW} "שגיאה: התקנת WebView2 נכשלה עם קוד שגיאה $1"
|
||||
LangString webview2InstallSuccess ${LANG_HEBREW} "WebView2 הותקן בהצלחה"
|
||||
LangString deleteAppData ${LANG_HEBREW} "מחק את נתוני היישום"
|
||||
@@ -0,0 +1,27 @@
|
||||
LangString addOrReinstall ${LANG_ITALIAN} "Aggiungi/Reinstalla componenti"
|
||||
LangString alreadyInstalled ${LANG_ITALIAN} "Già installato"
|
||||
LangString alreadyInstalledLong ${LANG_ITALIAN} "${PRODUCTNAME} ${VERSION} è già installato. Seleziona l'operazione che vuoi eseguire e clicca Avanti per continuare."
|
||||
LangString appRunning ${LANG_ITALIAN} "{{product_name}} è in esecuzione! Chiudi e poi riprova."
|
||||
LangString appRunningOkKill ${LANG_ITALIAN} "{{product_name}} è in esecuzione!$\nSeleziona OK per chiuderlo"
|
||||
LangString chooseMaintenanceOption ${LANG_ITALIAN} "Seleziona l'operazione di manutenzione da eseguire."
|
||||
LangString choowHowToInstall ${LANG_ITALIAN} "Seleziona come vuoi installare ${PRODUCTNAME}."
|
||||
LangString createDesktop ${LANG_ITALIAN} "Crea scorciatoia sul Desktop"
|
||||
LangString dontUninstall ${LANG_ITALIAN} "Non disinstallare"
|
||||
LangString dontUninstallDowngrade ${LANG_ITALIAN} "Non disinstallare (Il downgrade senza la disinstallazione è disabilitato per questo installer)"
|
||||
LangString failedToKillApp ${LANG_ITALIAN} "Impossibile chiudere {{product_name}}. Chiudi e poi riprova"
|
||||
LangString installingWebview2 ${LANG_ITALIAN} "Installando WebView2..."
|
||||
LangString newerVersionInstalled ${LANG_ITALIAN} "Una versione più recente di ${PRODUCTNAME} è già installata! Non è consigliato installare una versione più vecchia. Se vuoi comunque procedere, è meglio prima disinstallare la versione corrente. Seleziona l'operazione che vuoi eseguire e clicca Avanti per continuare."
|
||||
LangString older ${LANG_ITALIAN} "più vecchia"
|
||||
LangString olderOrUnknownVersionInstalled ${LANG_ITALIAN} "Una versione $R4 di ${PRODUCTNAME} è installata nel tuo sistema. È consigliato che disinstalli la versione corrente prima di procedere all'installazione. Seleziona l'operazione che vuoi eseguire e clicca Avanti per continuare."
|
||||
LangString silentDowngrades ${LANG_ITALIAN} "I downgrade sono disabilitati per questo installer, impossibile procedere con l'installer silenzioso, usa invece l'installer con interfaccia grafica.$\n"
|
||||
LangString unableToUninstall ${LANG_ITALIAN} "Impossibile disinstallare!"
|
||||
LangString uninstallApp ${LANG_ITALIAN} "Disinstalla ${PRODUCTNAME}"
|
||||
LangString uninstallBeforeInstalling ${LANG_ITALIAN} "Disinstalla prima di installare"
|
||||
LangString unknown ${LANG_ITALIAN} "sconosciuta"
|
||||
LangString webview2AbortError ${LANG_ITALIAN} "Errore nell'installazione di WebView2! L'app non può funzionare senza. Prova a riavviare l'installer."
|
||||
LangString webview2DownloadError ${LANG_ITALIAN} "Errore: Il download di WebView2 è fallito - $0"
|
||||
LangString webview2DownloadSuccess ${LANG_ITALIAN} "Bootstrapper WebView2 scaricato con successo"
|
||||
LangString webview2Downloading ${LANG_ITALIAN} "Scaricando il bootstrapper WebView2..."
|
||||
LangString webview2InstallError ${LANG_ITALIAN} "Errore: L'installazione di WebView2 è fallita con il codice $1"
|
||||
LangString webview2InstallSuccess ${LANG_ITALIAN} "WebView2 installato correttamente"
|
||||
LangString deleteAppData ${LANG_ITALIAN} "Cancella i dati dell'applicazione"
|
||||
@@ -1,14 +1,14 @@
|
||||
LangString addOrReinstall ${LANG_JAPANESE} "コンポーネントの追加・再インストール"
|
||||
LangString alreadyInstalled ${LANG_JAPANESE} "既にインストールされています"
|
||||
LangString alreadyInstalledLong ${LANG_JAPANESE} "${PRODUCTNAME} ${VERSION} は既にインストールされています。実行したい操作を選択し、「次へ」をクリックして続行します。"
|
||||
LangString appRunning ${LANG_JAPANESE} "${PRODUCTNAME} は動作中です。動作中のプログラムを終了し、もう一度やり直してください。"
|
||||
LangString appRunningOkKill ${LANG_JAPANESE} "${PRODUCTNAME} は動作中です。$\n「OK」を押すと動作中のプログラムを終了します。"
|
||||
LangString appRunning ${LANG_JAPANESE} "{{product_name}} は動作中です。動作中のプログラムを終了し、もう一度やり直してください。"
|
||||
LangString appRunningOkKill ${LANG_JAPANESE} "{{product_name}} は動作中です。$\n「OK」を押すと動作中のプログラムを終了します。"
|
||||
LangString chooseMaintenanceOption ${LANG_JAPANESE} "メンテナンスオプションを選択して実行します。"
|
||||
LangString choowHowToInstall ${LANG_JAPANESE} "${PRODUCTNAME} のインストール方法を選択してください。"
|
||||
LangString createDesktop ${LANG_JAPANESE} "デスクトップショートカットを作成する"
|
||||
LangString dontUninstall ${LANG_JAPANESE} "アンインストールしない"
|
||||
LangString dontUninstallDowngrade ${LANG_JAPANESE} "アンインストールしない (このインストーラーでは、アンインストールをせずにダウングレードすることはできません)"
|
||||
LangString failedToKillApp ${LANG_JAPANESE} "${PRODUCTNAME} の終了に失敗しました。動作中のプログラムを終了し、もう一度やり直してください。"
|
||||
LangString failedToKillApp ${LANG_JAPANESE} "{{product_name}} の終了に失敗しました。動作中のプログラムを終了し、もう一度やり直してください。"
|
||||
LangString installingWebview2 ${LANG_JAPANESE} "WebView2 をインストール中です..."
|
||||
LangString newerVersionInstalled ${LANG_JAPANESE} "既に新しいバージョンの ${PRODUCTNAME} がインストールされています。古いバージョンをインストールすることは推奨されません。どうしてもこの旧バージョンをインストールしたい場合は、先に現行バージョンをアンインストールしておく方がよいでしょう。実行したい操作を選択し、「次へ」をクリックして続行します。"
|
||||
LangString older ${LANG_JAPANESE} "旧"
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
LangString addOrReinstall ${LANG_KOREAN} "컴포넌트 추가 및 재설치"
|
||||
LangString alreadyInstalled ${LANG_KOREAN} "이미 설치되어 있습니다"
|
||||
LangString alreadyInstalledLong ${LANG_KOREAN} "${PRODUCTNAME} ${VERSION}이(가) 이미 설치되어 있습니다. 수행하고자 하는 작업을 선택하고 '다음'을 클릭하여 계속합니다."
|
||||
LangString appRunning ${LANG_KOREAN} "${PRODUCTNAME}이(가) 실행 중입니다! 먼저 닫은 후 다시 시도하세요."
|
||||
LangString appRunningOkKill ${LANG_KOREAN} "${PRODUCTNAME}이(가) 실행 중입니다!$\n'OK'를 누르면 실행 중인 프로그램을 종료합니다."
|
||||
LangString appRunning ${LANG_KOREAN} "{{product_name}}이(가) 실행 중입니다! 먼저 닫은 후 다시 시도하세요."
|
||||
LangString appRunningOkKill ${LANG_KOREAN} "{{product_name}}이(가) 실행 중입니다!$\n'OK'를 누르면 실행 중인 프로그램을 종료합니다."
|
||||
LangString chooseMaintenanceOption ${LANG_KOREAN} "수행하려는 관리 옵션을 선택합니다."
|
||||
LangString choowHowToInstall ${LANG_KOREAN} "${PRODUCTNAME}의 설치 방법을 선택하세요.."
|
||||
LangString createDesktop ${LANG_KOREAN} "바탕화면 바로가기 만들기"
|
||||
LangString dontUninstall ${LANG_KOREAN} "제거하지 않기"
|
||||
LangString dontUninstallDowngrade ${LANG_KOREAN} "제거하지 않기 (이 설치 프로그램에서는 제거하지 않고 다운그레이드할 수 없습니다.)"
|
||||
LangString failedToKillApp ${LANG_KOREAN} "${PRODUCTNAME}을(를) 종료하지 못했습니다. 먼저 닫은 후 다시 시도하세요."
|
||||
LangString failedToKillApp ${LANG_KOREAN} "{{product_name}}을(를) 종료하지 못했습니다. 먼저 닫은 후 다시 시도하세요."
|
||||
LangString installingWebview2 ${LANG_KOREAN} "WebView2를 설치하는 중입니다..."
|
||||
LangString newerVersionInstalled ${LANG_KOREAN} "${PRODUCTNAME}의 최신 버전이 이미 설치되어 있습니다! 이전 버전을 설치하지 않는 것이 좋습니다. 이 이전 버전을 꼭 설치하려면 먼저 현재 버전을 제거하는 것이 좋습니다. 수행하려는 작업을 선택하고 '다음'을 클릭하여 계속합니다."
|
||||
LangString older ${LANG_KOREAN} "구"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user