diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e8d99f4..21324db 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,7 +3,10 @@ name: "Build YCode" on: push: branches: - - main + - "*" + pull_request: + branches: + - "*" workflow_dispatch: jobs: diff --git a/README.md b/README.md index 8e93271..e585cf3 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,19 @@ Coming soon... YCode is currently in development and not recommended for use. However, if you want to try it out, your feedback would be greatly appreciated! +**Windows is not supported yet (but it will be). Please do not open windows related issues at this time.** + You can download the latest build from [actions](https://github.com/nab138/YCode/actions/workflows/build.yml). ## How it works -- [Theos](https://theos.dev/) is used to build the project into an IPA. +- A darwin sdk is generated from a user provided copy of Xcode and darwin tools from [darwin-tools-linux-llvm](https://github.com/xtool-org/darwin-tools-linux-llvm) +- SPM uses the darwin sdk to build an executable which is packaged into an .app bundle. - [apple-private-apis](https://github.com/SideStore/apple-private-apis) is used to login to the Apple Account. Heavy additions have been made to support actually accessing the Developer APIs -- [ZSign](https://github.com/zhlynn/zsign) is used to sign the IPA. +- [ZSign](https://github.com/zhlynn/zsign) is used to sign the IPA with the certificate and provisioning profile acquired from the Apple Account - [idevice](https://github.com/jkcoxson/idevice) is used to install the IPA on the device. +- [xtool](https://xtool.sh) has been used as a reference for the implementation of the darwin sdk generation. - [Sideloader](https://github.com/Dadoum/Sideloader) has been heavily used as a reference for the implementation of the Apple Developer APIs and sideloading process. ## Progress @@ -41,11 +45,11 @@ You can download the latest build from [actions](https://github.com/nab138/YCode - [x] Code editor (monaco editor) - [x] Project Creation - [x] Project Templates +- [x] SwiftPM support - [ ] Swift LSP Support - [ ] UI to change makefile settings - [ ] Git integration - [ ] Debugging (more research needed) -- [ ] SwiftPM support (more research needed) ## What AI did diff --git a/bun.lockb b/bun.lockb index 2811088..37cbcd4 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 6189ebe..d591579 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -495,7 +495,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash 1.1.0", + "rustc-hash", "shlex", "syn 2.0.104", "which", @@ -707,7 +707,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02260d489095346e5cafd04dea8e8cb54d1d74fcd759022a9b72986ebe9a1257" dependencies = [ "serde", - "toml", + "toml 0.8.23", ] [[package]] @@ -814,46 +814,6 @@ dependencies = [ "libloading 0.8.8", ] -[[package]] -name = "clap" -version = "4.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "clap_lex" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" - [[package]] name = "cmake" version = "0.1.54" @@ -894,12 +854,6 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - [[package]] name = "convert_case" version = "0.4.0" @@ -1134,31 +1088,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c" dependencies = [ - "const-oid 0.7.1", -] - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid 0.9.6", - "der_derive", - "flagset", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "der_derive" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", + "const-oid", ] [[package]] @@ -1213,7 +1143,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", - "const-oid 0.9.6", "crypto-common", "subtle", ] @@ -1386,7 +1315,7 @@ dependencies = [ "cc", "memchr", "rustc_version", - "toml", + "toml 0.8.23", "vswhom", "winreg 0.55.0", ] @@ -1575,12 +1504,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "flagset" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" - [[package]] name = "flate2" version = "1.1.2" @@ -1927,10 +1850,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", - "wasm-bindgen", ] [[package]] @@ -1940,11 +1861,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", - "wasm-bindgen", ] [[package]] @@ -2120,6 +2039,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.3.1", + "indexmap 2.9.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -2281,7 +2219,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "httparse", @@ -2304,6 +2242,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2 0.4.10", "http 1.3.1", "http-body 1.0.1", "httparse", @@ -2342,7 +2281,6 @@ dependencies = [ "tokio", "tokio-rustls 0.26.2", "tower-service", - "webpki-roots 1.0.1", ] [[package]] @@ -2358,6 +2296,22 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.6.0", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.14" @@ -2377,9 +2331,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2 0.5.10", + "system-configuration 0.6.1", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -2542,28 +2498,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ea3cd76e0b220669d169436448b1ffd777a82802ceb58e3a9efe6e30134e989" dependencies = [ "base64 0.22.1", - "byteorder", - "bytes", "chrono", "env_logger", - "futures", - "indexmap 2.9.0", - "json", "log", - "ns-keyed-archive", "plist", - "rand 0.9.1", - "reqwest 0.12.20", - "rsa", "rustls 0.23.28", "serde", - "serde_json", - "sha2", "thiserror 2.0.12", "tokio", "tokio-rustls 0.26.2", - "uuid", - "x509-cert", ] [[package]] @@ -2819,12 +2762,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" - [[package]] name = "json-patch" version = "3.0.1" @@ -2920,9 +2857,6 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin 0.9.8", -] [[package]] name = "lazycell" @@ -2980,12 +2914,6 @@ dependencies = [ "windows-targets 0.53.2", ] -[[package]] -name = "libm" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" - [[package]] name = "libredox" version = "0.1.3" @@ -3056,12 +2984,6 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - [[package]] name = "mac" version = "0.1.1" @@ -3329,27 +3251,6 @@ dependencies = [ "serde", ] -[[package]] -name = "ns-keyed-archive" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bdf3b814bc8e07ff58c78487d00f693e655c7238d2bff0df2d6d8957f877af7" -dependencies = [ - "nskeyedarchiver_converter", - "plist", -] - -[[package]] -name = "nskeyedarchiver_converter" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f237a10fe003123daa55a74b63a0b0a65de1671b2d128711ffe5886891a8f77f" -dependencies = [ - "clap", - "plist", - "thiserror 2.0.12", -] - [[package]] name = "num" version = "0.4.3" @@ -3374,23 +3275,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-bigint-dig" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" -dependencies = [ - "byteorder", - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand 0.8.5", - "smallvec", - "zeroize", -] - [[package]] name = "num-complex" version = "0.4.6" @@ -3444,7 +3328,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", ] [[package]] @@ -3935,15 +3818,6 @@ dependencies = [ "sha2", ] -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - [[package]] name = "percent-encoding" version = "2.3.1" @@ -4107,35 +3981,14 @@ dependencies = [ "futures-io", ] -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der 0.7.10", - "pkcs8", - "spki 0.7.3", -] - [[package]] name = "pkcs7" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f7364e6d0e236473de91e042395d71e0e64715f99a60620b014a4a4c7d1619b" dependencies = [ - "der 0.5.1", - "spki 0.5.4", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der 0.7.10", - "spki 0.7.3", + "der", + "spki", ] [[package]] @@ -4332,61 +4185,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "quinn" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash 2.1.1", - "rustls 0.23.28", - "socket2 0.5.10", - "thiserror 2.0.12", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" -dependencies = [ - "bytes", - "getrandom 0.3.3", - "lru-slab", - "rand 0.9.1", - "ring 0.17.14", - "rustc-hash 2.1.1", - "rustls 0.23.28", - "rustls-pki-types", - "slab", - "thiserror 2.0.12", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2 0.5.10", - "tracing", - "windows-sys 0.59.0", -] - [[package]] name = "quote" version = "1.0.40" @@ -4641,12 +4439,12 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.32", "hyper-rustls 0.24.2", - "hyper-tls", + "hyper-tls 0.5.0", "ipnet", "js-sys", "log", @@ -4661,7 +4459,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper 0.1.2", - "system-configuration", + "system-configuration 0.5.1", "tokio", "tokio-native-tls", "tokio-rustls 0.24.1", @@ -4671,7 +4469,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 0.25.4", + "webpki-roots", "winreg 0.50.0", ] @@ -4683,27 +4481,30 @@ checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" dependencies = [ "base64 0.22.1", "bytes", + "encoding_rs", "futures-core", "futures-util", + "h2 0.4.10", "http 1.3.1", "http-body 1.0.1", "http-body-util", "hyper 1.6.0", "hyper-rustls 0.27.7", + "hyper-tls 0.6.0", "hyper-util", "js-sys", "log", + "mime", + "native-tls", "percent-encoding", "pin-project-lite", - "quinn", - "rustls 0.23.28", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper 1.0.2", "tokio", - "tokio-rustls 0.26.2", + "tokio-native-tls", "tokio-util", "tower", "tower-http", @@ -4713,7 +4514,6 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 1.0.1", ] [[package]] @@ -4750,7 +4550,7 @@ dependencies = [ "cc", "libc", "once_cell", - "spin 0.5.2", + "spin", "untrusted 0.7.1", "web-sys", "winapi", @@ -4770,27 +4570,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rsa" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" -dependencies = [ - "const-oid 0.9.6", - "digest", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core 0.6.4", - "sha2", - "signature", - "spki 0.7.3", - "subtle", - "zeroize", -] - [[package]] name = "rustc-demangle" version = "0.1.25" @@ -4803,12 +4582,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - [[package]] name = "rustc_version" version = "0.4.1" @@ -4891,7 +4664,6 @@ dependencies = [ "aws-lc-rs", "log", "once_cell", - "ring 0.17.14", "rustls-pki-types", "rustls-webpki 0.103.3", "subtle", @@ -4913,7 +4685,6 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ - "web-time", "zeroize", ] @@ -5169,6 +4940,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -5313,16 +5093,6 @@ dependencies = [ "libc", ] -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core 0.6.4", -] - [[package]] name = "simd-adler32" version = "0.3.7" @@ -5427,29 +5197,13 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "spki" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27" dependencies = [ - "der 0.5.1", -] - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der 0.7.10", + "der", ] [[package]] @@ -5580,7 +5334,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", - "system-configuration-sys", + "system-configuration-sys 0.5.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.9.4", + "system-configuration-sys 0.6.0", ] [[package]] @@ -5593,6 +5358,16 @@ dependencies = [ "libc", ] +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -5602,7 +5377,7 @@ dependencies = [ "cfg-expr", "heck 0.5.0", "pkg-config", - "toml", + "toml 0.8.23", "version-compare", ] @@ -5661,6 +5436,17 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "target-lexicon" version = "0.12.16" @@ -5736,7 +5522,7 @@ dependencies = [ "serde_json", "tauri-utils", "tauri-winres", - "toml", + "toml 0.8.23", "walkdir", ] @@ -5794,7 +5580,7 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "toml", + "toml 0.8.23", "walkdir", ] @@ -5836,7 +5622,7 @@ dependencies = [ "tauri-plugin", "tauri-utils", "thiserror 2.0.12", - "toml", + "toml 0.8.23", "url", ] @@ -5979,7 +5765,7 @@ dependencies = [ "serde_with", "swift-rs", "thiserror 2.0.12", - "toml", + "toml 0.8.23", "url", "urlpattern", "uuid", @@ -5994,7 +5780,7 @@ checksum = "e8d321dbc6f998d825ab3f0d62673e810c861aac2d0de2cc2c395328f1d113b4" dependencies = [ "embed-resource", "indexmap 2.9.0", - "toml", + "toml 0.8.23", ] [[package]] @@ -6108,42 +5894,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tinyvec" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tls_codec" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" -dependencies = [ - "tls_codec_derive", - "zeroize", -] - -[[package]] -name = "tls_codec_derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - [[package]] name = "tokio" version = "1.45.1" @@ -6215,7 +5965,7 @@ dependencies = [ "tokio", "tokio-rustls 0.24.1", "tungstenite", - "webpki-roots 0.25.4", + "webpki-roots", ] [[package]] @@ -6238,11 +5988,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "toml_edit 0.22.27", ] +[[package]] +name = "toml" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac" +dependencies = [ + "indexmap 2.9.0", + "serde", + "serde_spanned 1.0.0", + "toml_datetime 0.7.0", + "toml_parser", + "toml_writer", + "winnow 0.7.11", +] + [[package]] name = "toml_datetime" version = "0.6.11" @@ -6252,6 +6017,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +dependencies = [ + "serde", +] + [[package]] name = "toml_edit" version = "0.19.15" @@ -6259,7 +6033,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap 2.9.0", - "toml_datetime", + "toml_datetime 0.6.11", "winnow 0.5.40", ] @@ -6270,7 +6044,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" dependencies = [ "indexmap 2.9.0", - "toml_datetime", + "toml_datetime 0.6.11", "winnow 0.5.40", ] @@ -6282,18 +6056,33 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap 2.9.0", "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "toml_write", "winnow 0.7.11", ] +[[package]] +name = "toml_parser" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30" +dependencies = [ + "winnow 0.7.11", +] + [[package]] name = "toml_write" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "toml_writer" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" + [[package]] name = "tower" version = "0.5.2" @@ -6751,16 +6540,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "webkit2gtk" version = "2.0.1" @@ -6821,15 +6600,6 @@ version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" -[[package]] -name = "webpki-roots" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8782dd5a41a24eed3a4f40b606249b3e236ca61adf1f25ea4d45c73de122b502" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "webview2-com" version = "0.37.0" @@ -7008,6 +6778,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-registry" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1da3e436dc7653dfdf3da67332e22bff09bb0e28b0239e1624499c7830842e" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -7450,17 +7231,13 @@ dependencies = [ ] [[package]] -name = "x509-cert" -version = "0.2.5" +name = "xattr" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909" dependencies = [ - "const-oid 0.9.6", - "der 0.7.10", - "sha1", - "signature", - "spki 0.7.3", - "tls_codec", + "libc", + "rustix 1.0.7", ] [[package]] @@ -7487,6 +7264,7 @@ name = "y-code" version = "0.0.1" dependencies = [ "dircpy", + "flate2", "futures", "hex", "icloud_auth", @@ -7495,10 +7273,13 @@ dependencies = [ "once_cell", "openssl", "plist", + "regex", + "reqwest 0.12.20", "serde", "serde_json", "sha1", "sha2", + "tar", "tauri", "tauri-build", "tauri-plugin-dialog", @@ -7506,6 +7287,7 @@ dependencies = [ "tauri-plugin-opener", "tauri-plugin-shell", "tauri-plugin-store", + "toml 0.9.2", "uuid", "walkdir", "zip", @@ -7714,20 +7496,6 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] [[package]] name = "zerotrie" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 25be551..375cc28 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -19,7 +19,7 @@ tauri-plugin-fs = { version = "2", features= ["watch"] } tauri-plugin-dialog = "2" tauri-plugin-store = "2" tauri-plugin-opener = "2" -idevice = { version = "0.1.35", features = ["full"] } +idevice = { version = "0.1.35", features = ["usbmuxd", "afc", "installation_proxy"] } futures = "0.3.31" keyring = "2" icloud_auth = {path = "./apple-private-apis/icloud-auth" } @@ -33,6 +33,11 @@ zip = { version = "4.1.0", default-features = false, features = ["deflate"] } uuid = "1.17.0" walkdir = "2.5.0" dircpy = "0.3.19" +tar = "0.4.44" +reqwest = "0.12.20" +flate2 = "1.1.2" +regex = "1" +toml = "0.9.2" [features] # This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!! diff --git a/src-tauri/install_theos.sh b/src-tauri/install_theos.sh deleted file mode 100644 index 513b14b..0000000 --- a/src-tauri/install_theos.sh +++ /dev/null @@ -1,297 +0,0 @@ -#!/usr/bin/env bash - -# Error codes + association: -# 1 - Running as root -# 2 - Unsupported platform -# 3 - Dependency issue -# 4 - Unsupported shell -# 5 - Setting $THEOS failed -# 6 - Theos clone failed -# 7 - Toolchain install failed -# 8 - SDK install failed -# 9 - Checkra1n '/opt' setup failed -# 10 - WSL1 fakeroot->fakeroot-tcp failed -# 11 - Enabling Linux binary compat on FreeBSD failed - -set -e - -# Pretty print -special() { - printf "\e[0;34m==> \e[1;34mTheos Installer:\e[m %s\n" "$1" -} - -update() { - printf "\n\e[0;36m==> \e[1;36m%s\e[m\n" "$1" -} - -common() { - printf "\n\e[0;37m==> \e[1;37m%s\e[m\n" "$1" -} - -error() { - printf "\e[0;31m==> \e[1;31m%s\e[m\n" "$1" -} - - -# Root is no bueno -if [[ $EUID -eq 0 ]]; then - error "Theos should NOT be installed with or run as root (su/sudo)!" - error " - Please re-run the installer as a non-root user." - exit 1 -fi - - -# Common vars -PLATFORM=$(uname) -CSHELL="${SHELL##*/}" -SHELL_ENV="unknown" -if [[ $CSHELL == sh || $CSHELL == bash || $CSHELL == dash ]]; then - # Bash prioritizes bashrc > bash_profile > profile - if [[ -f $HOME/.bashrc ]]; then - SHELL_ENV="$HOME/.bashrc" - elif [[ -f $HOME/.bash_profile ]]; then - SHELL_ENV="$HOME/.bash_profile" - else - SHELL_ENV="$HOME/.profile" - fi -elif [[ $CSHELL == zsh ]]; then - # Zsh prioritizes zshenv > zprofile > zshrc - if [[ -f $HOME/.zshenv ]]; then - SHELL_ENV="$HOME/.zshenv" - elif [[ -f $HOME/.zprofile ]]; then - SHELL_ENV="$HOME/.zprofile" - else - SHELL_ENV="$HOME/.zshrc" - fi -# TODO -# elif [[ $CSHELL == csh ]]; then -# SHELL_ENV="$HOME/.cshrc" -fi - -run_with_sudo() { - if [[ $(uname -r | sed -n 's/.*\( *Microsoft *\).*/\L\1/ip') == microsoft ]]; then - echo "$SUDO_PASSWORD" | sudo -S "$@" - else - pkexec "$@" - fi -} - -set_theos() { - # Check for $THEOS env var - update "Checking for \$THEOS environment variable..." - if ! [[ -z $THEOS ]]; then - update "\$THEOS is already set to '$THEOS'. Nothing to do here." - else - update "\$THEOS has not been set. Setting now..." - - if [[ $SHELL_ENV == unknown ]]; then - error "Current shell ($CSHELL) is unsupported by this installer. Please set the THEOS environment variable to '~/theos' manually before proceeding." - exit 4 - fi - echo "export THEOS=~/theos" >> "$SHELL_ENV" - export THEOS=~/theos - fi -} - -get_theos() { - # Get Theos - update "Checking for Theos install..." - if [[ -d $THEOS && $(ls -A "$THEOS") ]]; then - update "Theos appears to already be installed. Checking for updates..." - $THEOS/bin/update-theos - else - update "Theos does not appear to be installed. Cloning now..." - git clone --recursive https://github.com/theos/theos.git $THEOS \ - && update "Git clone of Theos was successful!" \ - || (error "Theos git clone command seems to have encountered an error. Please see the log above."; exit 6) - fi -} - -get_sdks() { - # Get patched sdks - update "Checking for patched SDKs..." - if [[ -d $THEOS/sdks/ && $(ls -A "$THEOS/sdks/" | grep sdk) ]]; then - update "SDKs appear to already be installed." - else - update "SDKs do not appear to be installed. Installing now..." - # Grab latest for provided platforms - urls=$(curl https://api.github.com/repos/theos/sdks/releases/latest | grep download_url | sed 's/.*: "\(.*\)"/\1/') - ios_url=$(echo "$urls" | grep 'iPhoneOS' | sort -V | tail -n1) - tvos_url=$(echo "$urls" | grep 'AppleTVOS' | sort -V | tail -n1) - curl -L "$ios_url" | tar -xJv -C "$THEOS/sdks" - curl -L "$tvos_url" | tar -xJv -C "$THEOS/sdks" - - if [[ -d $THEOS/sdks/ && $(ls -A "$THEOS/sdks/" | grep sdk) ]]; then - update "SDKs successfully installed!" - else - error "Something appears to have gone wrong. Please try again." - exit 8 - fi - fi -} - -linux() { - # Determine distro - DISTRO="unknown" - if [[ -x $(command -v apt) ]]; then - DISTRO="debian" - elif [[ -x $(command -v pacman) ]]; then - DISTRO="arch" - elif [[ -x $(command -v dnf) ]]; then - DISTRO="redhat" - elif [[ -x $(command -v zypper) ]]; then - DISTRO="suse" - fi - - # Check for pkexec (not installed by default on some distros) - if ! [[ -x $(command -v pkexec) ]] && ! [[ $(uname -r | sed -n 's/.*\( *Microsoft *\).*/\L\1/ip') == microsoft ]]; then - error "Please install 'pkexec' before proceeding with the installation." - exit 3 - fi - - # Dependencies - update "Preparing to install dependencies. Please enter your password if prompted:" - case $DISTRO in - debian) - run_with_sudo apt update || true - run_with_sudo apt install -y build-essential fakeroot rsync curl perl zip git libxml2 \ - && update "Dependencies have been successfully installed!" \ - || (error "Dependency install command seems to have encountered an error. Your password may have been incorrect."; exit 3) - ;; - arch) - run_with_sudo pacman -Syu || true - run_with_sudo pacman -S --needed --noconfirm base-devel libbsd fakeroot openssl rsync curl perl zip git libxml2 \ - && update "Dependencies have been successfully installed!" \ - || (error "Dependency install command seems to have encountered an error. Your password may have been incorrect."; exit 3) - ;; - redhat) - run_with_sudo dnf --refresh || true - run_with_sudo dnf group install -y "C Development Tools and Libraries" \ - && update "Dependencies have been successfully installed!" \ - || (error "Dependency install command seems to have encountered an error. Your password may have been incorrect."; exit 3) - run_with_sudo dnf install -y fakeroot lzma libbsd rsync curl perl zip git libxml2 \ - && update "Other dependencies have been successfully installed!" \ - || (error "Other Dependency install command seems to have encountered an error. Your password may have been incorrect."; exit 3) - ;; - suse) - run_with_sudo zypper refresh || true - run_with_sudo zypper install -y -t pattern devel_basis \ - && update "Dependencies have been successfully installed!" \ - || (error "Dependency install command seems to have encountered an error. Your password may have been incorrect."; exit 3) - run_with_sudo zypper install -y fakeroot libbsd0 rsync curl perl zip git libxml2 \ - && update "Other dependencies have been successfully installed!" \ - || (error "Other Dependency install command seems to have encountered an error. Your password may have been incorrect."; exit 3) - ;; - *) - error "The dependencies for your distro are unknown to this installer. Note that they will need to be determined before Theos can be installed and/or function properly." - common "On Debian-based distros, the necessary dependencies are: build-essential fakeroot rsync curl perl git libxml2 and libtinfo5 (non-swift toolchain) or libz3-dev (swift toolchain)." - common "Additional dependencies may also be required depending on what your distro provides." - ;; - esac - - # Check for WSL - update "Checking for WSL..." - if [[ $(uname -r | sed -n 's/.*\( *Microsoft *\).*/\L\1/ip') == microsoft ]]; then - VERSION=$(uname -r | sed 's/.*\([[:digit:]]\)[[:space:]]*/\1/') - if [[ $VERSION -eq 1 ]]; then - update "WSL1! Need to fix fakeroot..." - run_with_sudo update-alternatives --set fakeroot /usr/bin/fakeroot-tcp \ - && update "fakeroot fixed!" \ - || (error "fakeroot fix seems to have encountered an error. Please see the log above."; exit 10) - else - update "WSL2! Nothing to do here." - fi - else - update "Seems you're not using WSL. Moving on..." - fi - - set_theos - get_theos - - # Get a toolchain - update "Checking for iOS toolchain..." - if [[ -d $THEOS/toolchain/linux/iphone/ && $(ls -A "$THEOS/toolchain/linux/iphone") ]]; then - update "A toolchain appears to already be installed." - else - update "A toolchain does not appear to be installed." - case $DISTRO in - debian) - run_with_sudo apt install -y libz3-dev zstd - ;; - arch) - run_with_sudo pacman -S --needed --noconfirm libedit z3 zstd - # libz3-dev equivalent is z3 and we need to create lib version queried - LATEST_LIBZ3="$(ls -v /usr/lib/ | grep libz3 | tail -n 1)" - run_with_sudo ln -sf /usr/lib/$LATEST_LIBZ3 /usr/lib/libz3.so.4 - # toolchain looks for a specific libedit - LATEST_LIBEDIT="$(ls -v /usr/lib/ | grep libedit | tail -n 1)" - run_with_sudo ln -sf /usr/lib/$LATEST_LIBEDIT /usr/lib/libedit.so.2 - ;; - redhat) - run_with_sudo dnf install -y z3-libs zstd - # libz3-dev equivalent is z3-libs and ... - LATEST_LIBZ3="$(ls -v /usr/lib64/ | grep libz3 | tail -n 1)" - run_with_sudo ln -sf /usr/lib64/$LATEST_LIBZ3 /usr/lib64/libz3.so.4 - # toolchain looks for a specific libedit - LATEST_LIBEDIT="$(ls -v /usr/lib64/ | grep libedit | tail -n 1)" - run_with_sudo ln -sf /usr/lib64/$LATEST_LIBEDIT /usr/lib64/libedit.so.2 - ;; - suse) - run_with_sudo zypper install -y $(zypper search libz3 | tail -n 1 | cut -d "|" -f2) zstd - # libz3-dev equivalent is libz3-* and ... - LATEST_LIBZ3="$(ls -v /usr/lib64/ | grep libz3 | tail -n 1)" - run_with_sudo ln -sf /usr/lib64/$LATEST_LIBZ3 /usr/lib64/libz3.so.4 - # toolchain looks for a specific libedit - LATEST_LIBEDIT="$(ls -v /usr/lib64/ | grep libedit | tail -n 1)" - run_with_sudo ln -sf /usr/lib64/$LATEST_LIBEDIT /usr/lib64/libedit.so.2 - ;; - esac - mkdir -p $THEOS/toolchain/linux/iphone $THEOS/toolchain/swift - # If not ubuntu, send a warning - if [[ $DISTRO != debian ]]; then - common "Theos toolchain for Swift is only supported on Ubuntu. You may need to install the toolchain manually." - fi - # Check if the system is Linux Mint - if [ -f /etc/upstream-release/lsb-release ]; then - # Source the lsb-release file - . /etc/upstream-release/lsb-release - elif command -v lsb_release &> /dev/null; then - # Get the release number - DISTRIB_RELEASE=$(lsb_release -rs) - else - # Print a warning and use a default value - common "Warning: Could not determine Ubuntu version. Using default release number." - DISTRIB_RELEASE="20.04" - fi - - # If its 24.04, use 22.04 - if [[ $DISTRIB_RELEASE == 24.04 ]]; then - DISTRIB_RELEASE="22.04" - fi - - # Print the release number - update "Downloading toolchain ubuntu$DISTRIB_RELEASE" - - curl -sL https://github.com/kabiroberai/swift-toolchain-linux/releases/download/v2.3.0/swift-5.8-ubuntu$DISTRIB_RELEASE.tar.xz | tar -xJf - -C $THEOS/toolchain - ln -s $THEOS/toolchain/linux/iphone $THEOS/toolchain/swift - - # Confirm that toolchain is usable - if [[ -x $THEOS/toolchain/linux/iphone/bin/clang ]]; then - update "Successfully installed the toolchain!" - else - error "Something appears to have gone wrong -- the toolchain is not accessible. Please try again." - exit 7 - fi - fi - - get_sdks -} - - -if [[ ${PLATFORM,,} == linux ]]; then - linux -else - error "'$PLATFORM' is currently unsupported by YCode. Currently, only linux is supported." - exit 2 -fi -special "Theos has been successfully installed! Please restart YCode." \ No newline at end of file diff --git a/src-tauri/src/builder/config.rs b/src-tauri/src/builder/config.rs new file mode 100644 index 0000000..28c6dd8 --- /dev/null +++ b/src-tauri/src/builder/config.rs @@ -0,0 +1,117 @@ +use std::{path::PathBuf, process::Command}; + +use serde::{Deserialize, Serialize}; + +use crate::builder::swift::swift_bin; + +pub const FORMAT_VERSION: u32 = 1; + +pub struct BuildSettings { + pub debug: bool, +} + +// TODO: Min ios version, etc. +pub struct ProjectConfig { + pub product: String, + pub version_num: String, + pub version_string: String, + pub bundle_id: String, + pub project_path: PathBuf, +} + +#[derive(Deserialize, Serialize)] +struct TomlConfig { + pub format_version: u32, + pub project: ProjectTomlConfig, +} + +#[derive(Deserialize, Serialize)] +struct ProjectTomlConfig { + pub version_num: String, + pub version_string: String, + pub bundle_id: String, +} + +// TODO: Check platforms +#[derive(Deserialize)] +struct SwiftPackageDump { + name: String, + targets: Vec, +} + +// TODO: Resources +#[derive(Deserialize)] +struct SwiftPackageTarget { + name: String, +} + +impl ProjectConfig { + pub fn load(project_path: PathBuf, toolchain_path: &str) -> Result { + let toml_config = TomlConfig::load_or_default(project_path.clone())?; + let swift = swift_bin(toolchain_path)?; + let raw_package = Command::new(swift) + .arg("package") + .arg("dump-package") + .current_dir(&project_path) + .output() + .map_err(|e| format!("Failed to execute swift command: {}", e))?; + if !raw_package.status.success() { + return Err(format!( + "Failed to dump package: {}", + String::from_utf8_lossy(&raw_package.stderr) + )); + } + let package: SwiftPackageDump = serde_json::from_slice(&raw_package.stdout) + .map_err(|e| format!("Failed to parse package dump: {}", e))?; + + Ok(ProjectConfig { + product: package.name, + version_num: toml_config.project.version_num, + version_string: toml_config.project.version_string, + bundle_id: toml_config.project.bundle_id, + project_path, + }) + } +} + +impl TomlConfig { + pub fn default(bundle_id: &str) -> Self { + TomlConfig { + format_version: FORMAT_VERSION, + project: ProjectTomlConfig { + version_num: "1".to_string(), + version_string: "1.0.0".to_string(), + bundle_id: bundle_id.to_string(), + }, + } + } + + pub fn load_or_default(project_path: PathBuf) -> Result { + if project_path.exists() { + Self::load(project_path) + } else { + let config = Self::default("com.example.myapp"); + config.save(project_path)?; + Ok(config) + } + } + + fn load(project_path: PathBuf) -> Result { + let content = + std::fs::read_to_string(project_path.join("ycode.toml")).map_err(|e| e.to_string())?; + let config: TomlConfig = toml::from_str(&content).map_err(|e| e.to_string())?; + if config.format_version != FORMAT_VERSION { + return Err(format!( + "Unsupported format version: {}, expected: {}", + config.format_version, FORMAT_VERSION + )); + } + Ok(config) + } + + pub fn save(&self, project_path: PathBuf) -> Result<(), String> { + let content = toml::to_string(self).map_err(|e| e.to_string())?; + std::fs::write(project_path.join("ycode.toml"), content).map_err(|e| e.to_string())?; + Ok(()) + } +} diff --git a/src-tauri/src/builder/mod.rs b/src-tauri/src/builder/mod.rs new file mode 100644 index 0000000..f1a5fc4 --- /dev/null +++ b/src-tauri/src/builder/mod.rs @@ -0,0 +1,4 @@ +pub mod config; +pub mod packer; +pub mod sdk; +pub mod swift; diff --git a/src-tauri/src/builder/packer.rs b/src-tauri/src/builder/packer.rs new file mode 100644 index 0000000..0ad8041 --- /dev/null +++ b/src-tauri/src/builder/packer.rs @@ -0,0 +1,69 @@ +use std::{fs, path::PathBuf}; + +use dircpy::CopyBuilder; + +use crate::builder::config::{BuildSettings, ProjectConfig}; + +pub fn pack( + project_path: PathBuf, + config: &ProjectConfig, + build_settings: &BuildSettings, +) -> Result { + let workdir = project_path.join(".ycode"); + if !workdir.exists() { + std::fs::create_dir_all(&workdir) + .map_err(|e| format!("Failed to create work directory: {}", e))?; + } + let app_path = workdir.join(format!("{}.app", config.product)); + if app_path.exists() { + std::fs::remove_dir_all(&app_path) + .map_err(|e| format!("Failed to remove existing app directory: {}", e))?; + } + std::fs::create_dir_all(&app_path) + .map_err(|e| format!("Failed to create app directory: {}", e))?; + + let exec = project_path + .join(".build") + .join("arm64-apple-ios") + .join(if build_settings.debug { + "debug" + } else { + "release" + }) + .join(&config.product); + + if !exec.exists() { + return Err(format!("Executable not found at: {}", exec.display())); + } + + fs::copy(exec, app_path.join(&config.product)) + .map_err(|e| format!("Failed to copy executable: {}", e))?; + + // TODO: Create default Info.plist if it doesn't exist + let info_plist = project_path.join("Info.plist"); + if !info_plist.exists() { + return Err(format!("Info.plist not found at: {}", info_plist.display())); + } + + let info_content = fs::read_to_string(&info_plist) + .map_err(|e| format!("Failed to read Info.plist: {}", e))? + .replace("[[bundle_id]]", &config.bundle_id) + .replace("[[product]]", &config.product) + .replace("[[version_num]]", &config.version_num) + .replace("[[version_string]]", &config.version_string); + fs::write(&app_path.join("Info.plist"), info_content) + .map_err(|e| format!("Failed to write Info.plist: {}", e))?; + + let resources = project_path.join("Resources"); + + if !resources.exists() { + std::fs::create_dir_all(&resources) + .map_err(|e| format!("Failed to create Resources directory: {}", e))?; + } + + CopyBuilder::new(&resources, &app_path) + .run() + .map_err(|e| format!("Failed to copy resources: {}", e))?; + + Ok(app_path) +} diff --git a/src-tauri/src/builder/sdk.rs b/src-tauri/src/builder/sdk.rs new file mode 100644 index 0000000..faac690 --- /dev/null +++ b/src-tauri/src/builder/sdk.rs @@ -0,0 +1,647 @@ +// Reference: https://github.com/xtool-org/xtool/blob/main/Sources/XToolSupport/SDKBuilder.swift +use regex::Regex; +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; +use std::fs; +use std::os::unix::fs::symlink; +use std::path::{Component, Path, PathBuf}; +use std::process::Command; +use tauri::{AppHandle, Manager, Window}; + +use crate::builder::swift::{swift_bin, validate_toolchain}; +use crate::operation::Operation; + +const DARWIN_TOOLS_VERSION: &str = "1.0.1"; + +#[tauri::command] +pub async fn install_sdk_operation( + app: AppHandle, + window: Window, + xcode_path: String, + toolchain_path: String, +) -> Result<(), String> { + let op = Operation::new("install_sdk".to_string(), &window); + let work_dir = std::env::temp_dir().join("DarwinSDKBuild"); + let res = install_sdk_internal(app, xcode_path, toolchain_path, work_dir.clone(), &op).await; + op.start("cleanup")?; + let cleanup_result = if work_dir.exists() { + fs::remove_dir_all(&work_dir) + } else { + Ok(()) + }; + + let cleanup_result_for_match = cleanup_result + .as_ref() + .map(|_| ()) + .map_err(|e| format!("{}", e)); + + let cleanup_result = op.fail_if_err_map("cleanup", cleanup_result, |e| { + format!("Failed to remove temp dir: {}", e) + }); + + if cleanup_result.is_ok() { + op.complete("cleanup")?; + } + + match (res, cleanup_result_for_match) { + (Err(main_err), Err(cleanup_err)) => Err(format!( + "{main_err} (additionally, failed to clean up temp dir: {cleanup_err})" + )), + (Err(main_err), _) => Err(main_err), + (Ok(_), Err(cleanup_err)) => Err(format!( + "Install succeeded, but failed to clean up temp dir: {cleanup_err}" + )), + (Ok(val), Ok(_)) => Ok(val), + } +} +async fn install_sdk_internal( + app: AppHandle, + xcode_path: String, + toolchain_path: String, + work_dir: PathBuf, + op: &Operation<'_>, +) -> Result<(), String> { + op.start("create_stage")?; + if xcode_path.is_empty() || !xcode_path.ends_with(".xip") { + return op.fail("create_stage", "Xcode not found".to_string()); + } + if toolchain_path.is_empty() { + return op.fail("create_stage", "Toolchain not found".to_string()); + } + if !validate_toolchain(&toolchain_path) { + return op.fail("create_stage", "Invalid toolchain path".to_string()); + } + + let swift_bin = swift_bin(&toolchain_path)?; + let output = std::process::Command::new(swift_bin) + .arg("sdk") + .arg("remove") + .arg("darwin") + .output(); + if let Ok(output) = output { + if !output.status.success() && output.status.code() != Some(1) { + return op.fail( + "create_stage", + format!( + "Failed to remove existing darwin SDK: {}", + String::from_utf8_lossy(&output.stderr) + ), + ); + } + } + + let output_dir = work_dir.join("darwin.artifactbundle"); + if output_dir.exists() { + op.fail_if_err_map("create_stage", fs::remove_dir_all(&output_dir), |e| { + format!("Failed to remove existing output directory: {}", e) + })?; + } + op.fail_if_err_map("create_stage", fs::create_dir_all(&output_dir), |e| { + format!("Failed to create output directory: {}", e) + })?; + + op.move_on("create_stage", "install_toolset")?; + op.fail_if_err("install_toolset", install_toolset(&output_dir).await)?; + op.complete("install_toolset")?; + let dev = install_developer(&app, &output_dir, &xcode_path, op).await?; + op.start("write_metadata")?; + + let iphone_os_sdk = sdk(&dev, "iPhoneOS")?; + let mac_os_sdk = sdk(&dev, "MacOSX")?; + let iphone_simulator_sdk = sdk(&dev, "iPhoneSimulator")?; + + let info = " + { + \"schemaVersion\": \"1.0\", + \"artifacts\": { + \"darwin\": { + \"type\": \"swiftSDK\", + \"version\": \"0.0.1\", + \"variants\": [ + { + \"path\": \".\", + \"supportedTriples\": [\"aarch64-unknown-linux-gnu\", \"x86_64-unknown-linux-gnu\"] + } + ] + } + } + } + "; + op.fail_if_err_map( + "write_metadata", + fs::write(output_dir.join("info.json"), info), + |e| format!("Failed to write info.json: {}", e), + )?; + + let toolset = " + { + \"schemaVersion\": \"1.0\", + \"rootPath\": \"toolset/bin\", + \"linker\": { + \"path\": \"ld64.lld\" + }, + \"swiftCompiler\": { + \"extraCLIOptions\": [ + \"-use-ld=lld\" + ] + } + } + "; + op.fail_if_err_map( + "write_metadata", + fs::write(output_dir.join("toolset.json"), toolset), + |e| format!("Failed to write toolset.json: {}", e), + )?; + + let sdk_def = SDKDefinition { + schema_version: "4.0".to_string(), + target_triples: HashMap::from([ + ( + "arm64-apple-ios".to_string(), + Triple::from_sdk("iPhoneOS", &iphone_os_sdk), + ), + ( + "arm64-apple-ios-simulator".to_string(), + Triple::from_sdk("iPhoneSimulator", &iphone_simulator_sdk), + ), + ( + "x86_64-apple-ios-simulator".to_string(), + Triple::from_sdk("iPhoneSimulator", &iphone_simulator_sdk), + ), + ( + "arm64-apple-macos".to_string(), + Triple::from_sdk("MacOSX", &mac_os_sdk), + ), + ( + "x86_64-apple-macos".to_string(), + Triple::from_sdk("MacOSX", &mac_os_sdk), + ), + ]), + }; + + let sdk_def_path = output_dir.join("swift-sdk.json"); + op.fail_if_err_map( + "write_metadata", + fs::write( + sdk_def_path, + op.fail_if_err_map( + "write_metadata", + serde_json::to_string_pretty(&sdk_def), + |e| format!("Failed to serialize SDKDefinition: {}", e), + )?, + ), + |e| format!("Failed to write swift-sdk.json: {}", e), + )?; + + let sdk_version_path = output_dir.join("darwin-sdk-version.txt"); + op.fail_if_err_map( + "write_metadata", + fs::write(&sdk_version_path, "develop"), + |e| format!("Failed to write darwin-sdk-version.txt: {}", e), + )?; + op.move_on("write_metadata", "install_sdk")?; + + let path = PathBuf::from(toolchain_path); + let swift_path = path.join("usr").join("bin").join("swift"); + if !swift_path.exists() || !swift_path.is_file() { + return op.fail( + "install_sdk", + "Swift binary not found in toolchain".to_string(), + ); + } + + let output = op.fail_if_err_map( + "install_sdk", + std::process::Command::new(swift_path) + .arg("sdk") + .arg("install") + .arg(output_dir.to_string_lossy().to_string()) + .output(), + |e| format!("Failed to execute swift command: {}", e), + )?; + + if !output.status.success() { + return op.fail( + "install_sdk", + format!( + "Swift command failed: {}", + String::from_utf8_lossy(&output.stderr) + ), + ); + } + op.complete("install_sdk")?; + + Ok(()) +} + +fn sdk(dev: &PathBuf, platform: &str) -> Result { + let dir = dev.join(format!("Platforms/{}.platform/Developer/SDKs", platform)); + let regex = Regex::new(&format!(r"^{}\d+\.\d+\.sdk$", regex::escape(platform))) + .map_err(|e| format!("Invalid regex: {}", e))?; + + let entries = + fs::read_dir(&dir).map_err(|e| format!("Failed to read SDKs directory: {}", e))?; + + for entry in entries { + let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?; + let name = entry.file_name(); + let name_str = name.to_string_lossy(); + if regex.is_match(&name_str) { + return Ok(name_str.into_owned()); + } + } + + Err(format!("Could not find SDK for {}/{}", platform, platform)) +} + +async fn install_toolset(output_path: &PathBuf) -> Result<(), String> { + let toolset_dir = output_path.join("toolset"); + fs::create_dir_all(&toolset_dir) + .map_err(|e| format!("Failed to create toolset directory: {}", e))?; + + let arch = if cfg!(target_arch = "x86_64") { + "x86_64" + } else if cfg!(target_arch = "aarch64") { + "aarch64" + } else { + return Err("Unsupported architecture".to_string()); + }; + let toolset_url = format!( + "https://github.com/xtool-org/darwin-tools-linux-llvm/releases/download/v{}/toolset-{}.tar.gz", + DARWIN_TOOLS_VERSION, arch + ); + + let response = reqwest::get(&toolset_url) + .await + .map_err(|e| format!("Failed to download toolset: {}", e))?; + if !response.status().is_success() { + return Err(format!("Failed to download toolset: {}", response.status())); + } + let tar_gz = response + .bytes() + .await + .map_err(|e| format!("Failed to read response: {}", e))?; + let mut archive = tar::Archive::new(flate2::read::GzDecoder::new(&*tar_gz)); + archive + .unpack(&toolset_dir) + .map_err(|e| format!("Failed to extract toolset: {}", e))?; + Ok(()) +} + +async fn install_developer( + app: &AppHandle, + output_path: &PathBuf, + xcode_path: &str, + op: &Operation<'_>, +) -> Result { + op.start("extract_xip")?; + let dev_stage = output_path.join("DeveloperStage"); + op.fail_if_err_map("extract_xip", fs::create_dir_all(&dev_stage), |e| { + format!("Failed to create DeveloperStage directory: {}", e) + })?; + + let unxip_path = op.fail_if_err_map( + "extract_xip", + app.path() + .resolve("unxip", tauri::path::BaseDirectory::Resource), + |e| format!("Failed to resolve unxip path: {}", e), + )?; + + let status = Command::new(unxip_path) + .current_dir(&dev_stage) + .arg(xcode_path) + .output(); + if let Err(e) = status { + return op.fail("extract_xip", format!("Failed to run unxip: {}", e)); + } + let status = status.unwrap(); + if !status.status.success() { + return op.fail( + "extract_xip", + format!( + "{}\nProcess exited with code {}", + String::from_utf8_lossy(&status.stderr.trim_ascii()), + status.status.code().unwrap_or(0) + ), + ); + } + + let app_dirs = op + .fail_if_err_map("extract_xip", fs::read_dir(&dev_stage), |e| { + format!("Failed to read DeveloperStage directory: {}", e) + })? + .filter_map(Result::ok) + .filter(|entry| entry.path().extension().map_or(false, |ext| ext == "app")) + .collect::>(); + if app_dirs.len() != 1 { + return op.fail( + "extract_xip", + format!( + "Expected one .app in DeveloperStage, found {}", + app_dirs.len() + ), + ); + } + + op.move_on("extract_xip", "copy_files")?; + let app_path = app_dirs[0].path(); + let dev = output_path.join("Developer"); + op.fail_if_err_map("copy_files", fs::create_dir_all(&dev), |e| { + format!("Failed to create Developer directory: {}", e) + })?; + + let contents_developer = app_path.join("Contents/Developer"); + if !contents_developer.exists() { + return op.fail( + "copy_files", + "Contents/Developer not found in .app".to_string(), + ); + } + + op.fail_if_err( + "copy_files", + copy_developer(&contents_developer, &dev, Path::new("Contents/Developer")), + )?; + op.fail_if_err_map("copy_files", fs::remove_dir_all(&dev_stage), |e| { + format!("Failed to remove DeveloperStage directory: {}", e) + })?; + + for platform in ["iPhoneOS", "MacOSX", "iPhoneSimulator"] { + let lib = "../../../../../Library"; + let dest = dev.join(format!( + "Platforms/{}.platform/Developer/SDKs/{}.sdk/System/Library/Frameworks", + platform, platform + )); + + let links = [ + ( + "Testing.framework", + format!("{}/Frameworks/Testing.framework", lib), + ), + ( + "XCTest.framework", + format!("{}/Frameworks/XCTest.framework", lib), + ), + ( + "XCUIAutomation.framework", + format!("{}/Frameworks/XCUIAutomation.framework", lib), + ), + ( + "XCTestCore.framework", + format!("{}/PrivateFrameworks/XCTestCore.framework", lib), + ), + ]; + + for (name, target) in &links { + let link_path = dest.join(name); + op.fail_if_err_map("copy_files", symlink(target, &link_path), |e| { + format!( + "Failed to create symlink {:?} -> {:?}: {}", + link_path, target, e + ) + })?; + } + } + + op.complete("copy_files")?; + + Ok(dev) +} + +fn copy_developer(src: &Path, dst: &Path, rel: &Path) -> Result<(), String> { + use std::os::unix::fs as unix_fs; + + for entry in fs::read_dir(src).map_err(|e| format!("Failed to read dir: {}", e))? { + let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?; + let file_name = entry.file_name(); + let rel_path = rel.join(&file_name); + if !is_wanted(&rel_path) { + continue; + } + let src_path = entry.path(); + + let mut rel_components = rel_path.components(); + if let Some(c) = rel_components.next() { + if c.as_os_str() != "Contents" { + rel_components = rel_path.components(); + } + } + if let Some(c) = rel_components.next() { + if c.as_os_str() != "Developer" { + rel_components = rel_path.components(); + } + } + let dst_path = dst.join(rel_components.as_path()); + + let metadata = fs::symlink_metadata(&src_path) + .map_err(|e| format!("Failed to get metadata: {}", e))?; + + if metadata.file_type().is_symlink() { + let target = + fs::read_link(&src_path).map_err(|e| format!("Failed to read symlink: {}", e))?; + if let Some(parent) = dst_path.parent() { + fs::create_dir_all(parent) + .map_err(|e| format!("Failed to create parent dir: {}", e))?; + } + unix_fs::symlink(&target, &dst_path) + .map_err(|e| format!("Failed to create symlink: {}", e))?; + } else if metadata.is_dir() { + fs::create_dir_all(&dst_path).map_err(|e| format!("Failed to create dir: {}", e))?; + copy_developer(&src_path, dst, &rel_path)?; + } else if metadata.is_file() { + if let Some(parent) = dst_path.parent() { + fs::create_dir_all(parent) + .map_err(|e| format!("Failed to create parent dir: {}", e))?; + } + fs::copy(&src_path, &dst_path).map_err(|e| format!("Failed to copy file: {}", e))?; + } + } + Ok(()) +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct Triple { + sdk_root_path: String, + include_search_paths: Vec, + library_search_paths: Vec, + swift_resources_path: String, + swift_static_resources_path: String, + toolset_paths: Vec, +} + +impl Triple { + fn from_sdk(platform: &str, sdk: &str) -> Self { + Triple { + sdk_root_path: format!( + "Developer/Platforms/{}.platform/Developer/SDKs/{}", + platform, sdk + ), + include_search_paths: vec![format!( + "Developer/Platforms/{}.platform/Developer/usr/lib", + platform + )], + library_search_paths: vec![format!( + "Developer/Platforms/{}.platform/Developer/usr/lib", + platform + )], + swift_resources_path: format!( + "Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift" + ), + swift_static_resources_path: format!( + "Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift_static" + ), + toolset_paths: vec![format!("toolset.json")], + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct SDKDefinition { + schema_version: String, + target_triples: HashMap, +} + +#[derive(Debug, Clone)] +struct SDKEntry { + names: HashSet, + values: Vec, +} + +impl SDKEntry { + // empty = wildcard + fn new(names: HashSet, values: Vec) -> Self { + SDKEntry { names, values } + } + + fn from_name(name: &str, values: Vec) -> Self { + let mut set = HashSet::new(); + set.insert(name.to_string()); + SDKEntry::new(set, values) + } + + fn matches<'a, I>(&self, path: I) -> bool + where + I: Iterator + Clone, + { + let mut path_clone = path.clone(); + let first = path_clone.next(); + if first.is_none() { + return true; + } + let first = first.unwrap(); + if !self.names.is_empty() && !self.names.contains(first) { + return false; + } + if self.values.is_empty() { + return true; + } + let after_name = path_clone; + for value in &self.values { + if value.matches(after_name.clone()) { + return true; + } + } + false + } + + fn e(name: Option<&str>, values: Vec) -> SDKEntry { + if let Some(name) = name { + let parts: Vec<&str> = name.split('/').collect(); + let mut entry = SDKEntry::from_name(parts.last().unwrap(), values); + for part in parts.iter().rev().skip(1) { + entry = SDKEntry::from_name(part, vec![entry]); + } + entry + } else { + SDKEntry::new(HashSet::new(), values) + } + } +} + +// Build the wanted tree +fn wanted_sdk_entry() -> SDKEntry { + SDKEntry::e( + Some("Contents/Developer"), + vec![ + SDKEntry::e( + Some("Toolchains/XcodeDefault.xctoolchain/usr/lib"), + vec![ + SDKEntry::e(Some("swift"), vec![]), + SDKEntry::e(Some("swift_static"), vec![]), + SDKEntry::e(Some("clang"), vec![]), + ], + ), + SDKEntry::e( + Some("Platforms"), + ["iPhoneOS", "MacOSX", "iPhoneSimulator"] + .iter() + .map(|plat| { + SDKEntry::e( + Some(&format!("{}.platform/Developer", plat)), + vec![ + SDKEntry::e(Some("SDKs"), vec![]), + SDKEntry::e( + Some("Library"), + vec![ + SDKEntry::e(Some("Frameworks"), vec![]), + SDKEntry::e(Some("PrivateFrameworks"), vec![]), + ], + ), + SDKEntry::e(Some("usr/lib"), vec![]), + ], + ) + }) + .collect(), + ), + ], + ) +} + +fn is_wanted(path: &Path) -> bool { + let mut components: Vec = path + .components() + .filter_map(|c| match c { + Component::Normal(os) => Some(os.to_string_lossy().to_string()), + _ => None, + }) + .collect(); + + if let Some(first) = components.first() { + if first == "." { + components.remove(0); + } + } + if let Some(first) = components.first() { + if first.ends_with(".app") { + components.remove(0); + } + } + + if !wanted_sdk_entry().matches(components.iter().map(|s| s.as_str())) { + return false; + } + + if components.len() >= 10 + && components[9] == "prebuilt-modules" + && components.starts_with( + &[ + "Contents", + "Developer", + "Toolchains", + "XcodeDefault.xctoolchain", + "usr", + "lib", + "swift", + ] + .iter() + .map(|s| s.to_string()) + .collect::>(), + ) + { + return false; + } + + true +} diff --git a/src-tauri/src/builder/swift.rs b/src-tauri/src/builder/swift.rs new file mode 100644 index 0000000..3ed2600 --- /dev/null +++ b/src-tauri/src/builder/swift.rs @@ -0,0 +1,379 @@ +use crate::{ + builder::{ + config::{BuildSettings, ProjectConfig}, + packer::pack, + }, + device::DeviceInfo, + emit_error_and_return, + sideloader::sideload::sideload_app, +}; +use serde::{Deserialize, Serialize}; +use std::{ + io::{BufRead, BufReader}, + path::PathBuf, + process::{Command, Stdio}, + thread, +}; +use tauri::{Emitter, Window}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ToolchainResult { + pub swiftly_installed: bool, + pub swiftly_version: Option, + pub toolchains: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Toolchain { + pub version: String, + pub path: String, + pub is_swiftly: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +struct SwiftlyConfig { + pub installed_toolchains: Vec, + pub version: String, +} + +pub fn swift_bin(toolchain_path: &str) -> Result { + let path = PathBuf::from(toolchain_path); + if !path.exists() || !path.is_dir() { + return Err("Invalid toolchain path".to_string()); + } + let swift_path = path.join("usr").join("bin").join("swift"); + if !swift_path.exists() || !swift_path.is_file() { + return Err("Swift binary not found in toolchain".to_string()); + } + Ok(swift_path) +} + +#[tauri::command] +pub fn has_darwin_sdk(toolchain_path: &str) -> bool { + let swift_bin = swift_bin(toolchain_path); + if swift_bin.is_err() { + return false; + } + let swift_bin = swift_bin.unwrap(); + + let output = std::process::Command::new(swift_bin) + .arg("sdk") + .arg("list") + .output(); + if output.is_err() { + return false; + } + let output = output.unwrap(); + if !output.status.success() { + return false; + } + let output_str = String::from_utf8_lossy(&output.stdout); + + output_str.contains("darwin") +} + +#[tauri::command] +pub fn validate_toolchain(toolchain_path: &str) -> bool { + let swift_path = swift_bin(toolchain_path); + if swift_path.is_err() { + return false; + } + let swift_path = swift_path.unwrap(); + + let output = std::process::Command::new(swift_path) + .arg("--version") + .output(); + if output.is_err() { + return false; + } + let output = output.unwrap(); + if !output.status.success() { + return false; + } + + true +} + +#[tauri::command] +pub async fn get_toolchain_info( + toolchain_path: String, + is_swiftly: bool, +) -> Result { + if !validate_toolchain(&toolchain_path) { + return Err("Invalid toolchain path".to_string()); + } + let swift_path = swift_bin(&toolchain_path)?; + + let output = std::process::Command::new(swift_path) + .arg("--version") + .output() + .map_err(|e| format!("Failed to execute swift command: {}", e))?; + let version = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let version = version + .split_whitespace() + .nth(2) + .ok_or("Failed to parse swift version".to_string())? + .to_string(); + Ok(Toolchain { + version, + path: toolchain_path.clone(), + is_swiftly, + }) +} + +#[tauri::command] +pub async fn get_swiftly_toolchains() -> Result { + if !cfg!(target_os = "linux") { + return Err("YCode only supports linux right now".to_string()); + } + let swiftly_home_dir = get_swiftly_path(); + if let Some(_) = swiftly_home_dir { + let config = get_swiftly_config()?; + let toolchains_unfiltered: Vec = config + .installed_toolchains + .iter() + .map(|version| { + let path = PathBuf::from(swiftly_home_dir.as_ref().unwrap()) + .join("toolchains") + .join(version); + Toolchain { + version: version.clone(), + path: path.to_string_lossy().to_string(), + is_swiftly: true, + } + }) + .collect(); + + let mut toolchains = Vec::new(); + for toolchain in toolchains_unfiltered { + if validate_toolchain(&toolchain.path) { + toolchains.push(toolchain); + } + } + + return Ok(ToolchainResult { + swiftly_installed: true, + swiftly_version: Some(config.version), + toolchains, + }); + } else { + return Ok(ToolchainResult { + swiftly_installed: false, + swiftly_version: None, + toolchains: vec![], + }); + } +} + +fn get_swiftly_config() -> Result { + let swiftly_home_dir = get_swiftly_path().ok_or("Swiftly home directory not found")?; + + let config_path = format!("{}/config.json", swiftly_home_dir); + let content = std::fs::read_to_string(&config_path) + .map_err(|_| "Failed to read config file".to_string())?; + + // TODO: why? + let content = content.trim_end_matches('%').to_string(); + let config: SwiftlyConfig = serde_json::from_str(&content) + .map_err(|e| format!("Failed to parse config file: {}", e))?; + + Ok(config) +} + +fn get_swiftly_path() -> Option { + let swiftly_home_dir = std::env::var("SWIFTLY_HOME_DIR").unwrap_or_default(); + if !swiftly_home_dir.is_empty() { + return Some(swiftly_home_dir); + } + let home_dir = std::env::var("HOME").unwrap_or_default(); + if !home_dir.is_empty() { + let swiftly_path = format!("{}/.local/share/swiftly", home_dir); + if std::path::Path::new(&swiftly_path).exists() { + return Some(swiftly_path); + } + } + + None +} + +async fn build_swift_internal( + window: &Window, + folder: &str, + toolchain_path: &str, + build_settings: BuildSettings, + emit_exit_code: bool, +) -> Result { + let config = match ProjectConfig::load(PathBuf::from(&folder), &toolchain_path) { + Ok(config) => config, + Err(e) => { + return emit_error_and_return(&window, &format!("Failed to load project config: {}", e)) + } + }; + let swift_bin = swift_bin(&toolchain_path)?; + let mut cmd = Command::new(swift_bin); + cmd.arg("build") + .arg("-c") + .arg(if build_settings.debug { + "debug" + } else { + "release" + }) + .arg("--swift-sdk") + .arg("arm64-apple-ios") + .current_dir(&folder); + + pipe_command(&mut cmd, &window, emit_exit_code).await?; + + match pack(PathBuf::from(&folder), &config, &build_settings) { + Ok(app) => { + window + .emit("build-output", "Pack Success") + .expect("failed to send output"); + Ok(app) + } + Err(e) => emit_error_and_return(&window, &format!("Failed to pack app: {}", e)), + } +} + +#[tauri::command] +pub async fn build_swift( + window: tauri::Window, + folder: String, + toolchain_path: String, + debug: bool, +) -> Result<(), String> { + let build_settings = BuildSettings { debug }; + if !validate_toolchain(&toolchain_path) { + return Err("Invalid toolchain path".to_string()); + } + + let path = + build_swift_internal(&window, &folder, &toolchain_path, build_settings, true).await?; + + todo!("Zip into .ipa"); +} + +#[tauri::command] +pub async fn clean_swift( + window: tauri::Window, + folder: String, + toolchain_path: String, +) -> Result<(), String> { + let swift_bin = swift_bin(&toolchain_path)?; + let mut cmd = Command::new(swift_bin); + cmd.arg("package").arg("clean").current_dir(folder); + + window + .emit("build-output", "Cleaning...") + .expect("failed to send output"); + + pipe_command(&mut cmd, &window, true).await +} + +#[tauri::command] +pub async fn deploy_swift( + handle: tauri::AppHandle, + window: tauri::Window, + anisette_server: String, + device: DeviceInfo, + folder: String, + toolchain_path: String, + debug: bool, +) -> Result<(), String> { + let build_settings = BuildSettings { debug }; + if !validate_toolchain(&toolchain_path) { + return Err("Invalid toolchain path".to_string()); + } + + let app = + build_swift_internal(&window, &folder, &toolchain_path, build_settings, false).await?; + + sideload_app(&handle, window, anisette_server, device, app) + .await + .map_err(|e| format!("Failed to sideload app: {}", e)) +} + +pub async fn pipe_command( + cmd: &mut Command, + window: &tauri::Window, + emit_exit_code: bool, +) -> Result<(), String> { + let name = "build-output"; + cmd.stdout(Stdio::piped()); + cmd.stderr(Stdio::piped()); + + let mut command = match cmd.spawn() { + Ok(cmd) => cmd, + Err(_) => { + return emit_error_and_return(&window, "Failed to spawn build command"); + } + }; + + let stdout = match command.stdout.take() { + Some(out) => out, + None => { + return emit_error_and_return(&window, "Failed to get stdout"); + } + }; + + let stderr = match command.stderr.take() { + Some(err) => err, + None => { + return emit_error_and_return(&window, "Failed to get stderr"); + } + }; + + let stdout_handle = spawn_output_thread(stdout, window.clone(), name.to_string()); + let stderr_handle = spawn_output_thread(stderr, window.clone(), name.to_string()); + + stdout_handle.join().expect("stdout thread panicked"); + stderr_handle.join().expect("stderr thread panicked"); + + let exit_status = match command.wait() { + Ok(status) => status, + Err(_) => { + return emit_error_and_return(&window, "Failed to wait for command"); + } + }; + + let exit_code = exit_status.code().unwrap_or(1); + + if exit_code != 0 || emit_exit_code { + window + .emit(name, format!("command.done.{}", exit_code)) + .expect("failed to send output"); + } + + if exit_code != 0 { + return Err(format!("Command exited with code {}", exit_code)); + } + + Ok(()) +} + +fn spawn_output_thread( + reader: R, + window: tauri::Window, + name: String, +) -> std::thread::JoinHandle<()> { + thread::spawn(move || { + let reader = BufReader::new(reader); + for line in reader.lines() { + match line { + Ok(line) => { + window.emit(&name, line).expect("failed to send output"); + } + Err(err) => { + window + .emit(&name, "command.done.999".to_string()) + .expect("failed to send output"); + eprintln!("Error reading output: {}", err); + return; + } + } + } + }) +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 9c3fc50..888eb73 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,12 +1,15 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -#[macro_use] -mod theos; #[macro_use] mod device; #[macro_use] mod templates; +#[macro_use] +mod windows; +#[macro_use] +mod builder; +mod operation; mod sideloader; use device::refresh_idevice; @@ -16,10 +19,13 @@ use sideloader::apple_commands::{ }; use tauri::Emitter; use templates::create_template; -use theos::{ - build_theos, clean_theos, deploy_theos, has_theos, has_wsl, install_theos_linux, - install_theos_windows, is_windows, update_theos, + +use builder::sdk::install_sdk_operation; +use builder::swift::{ + build_swift, clean_swift, deploy_swift, get_swiftly_toolchains, get_toolchain_info, + has_darwin_sdk, validate_toolchain, }; +use windows::{has_wsl, is_windows}; fn main() { tauri::Builder::default() @@ -29,15 +35,11 @@ fn main() { .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_shell::init()) .invoke_handler(tauri::generate_handler![ - has_theos, - update_theos, - install_theos_linux, - install_theos_windows, is_windows, has_wsl, - build_theos, - deploy_theos, - clean_theos, + build_swift, + deploy_swift, + clean_swift, refresh_idevice, delete_stored_credentials, reset_anisette, @@ -47,12 +49,17 @@ fn main() { list_app_ids, delete_app_id, create_template, + get_swiftly_toolchains, + validate_toolchain, + get_toolchain_info, + install_sdk_operation, + has_darwin_sdk, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } -pub fn emit_error_and_return(window: &tauri::Window, msg: &str) -> Result<(), String> { +pub fn emit_error_and_return(window: &tauri::Window, msg: &str) -> Result { window.emit("build-output", msg.to_string()).ok(); window.emit("build-output", "command.done.999").ok(); Err(msg.to_string()) diff --git a/src-tauri/src/operation.rs b/src-tauri/src/operation.rs new file mode 100644 index 0000000..e8a23f8 --- /dev/null +++ b/src-tauri/src/operation.rs @@ -0,0 +1,88 @@ +use serde::Serialize; +use tauri::{Emitter, Window}; + +pub struct Operation<'a> { + id: String, + window: &'a Window, +} + +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +struct OperationUpdate<'a> { + update_type: &'a str, + step_id: &'a str, + extra_details: Option, +} + +impl<'a> Operation<'a> { + pub fn new(id: String, window: &'a Window) -> Operation<'a> { + Operation { id, window } + } + + pub fn move_on(&self, old_id: &str, new_id: &str) -> Result<(), String> { + self.complete(old_id)?; + self.start(new_id) + } + + pub fn start(&self, id: &str) -> Result<(), String> { + self.window + .emit( + &format!("operation_{}", self.id), + OperationUpdate { + update_type: "started", + step_id: id, + extra_details: None, + }, + ) + .map_err(|_| "Failed to emit status to frontend".to_string()) + } + + pub fn complete(&self, id: &str) -> Result<(), String> { + self.window + .emit( + &format!("operation_{}", self.id), + OperationUpdate { + update_type: "finished", + step_id: id, + extra_details: None, + }, + ) + .map_err(|_| "Failed to emit status to frontend".to_string()) + } + + pub fn fail(&self, id: &str, error: String) -> Result { + self.window + .emit( + &format!("operation_{}", self.id), + OperationUpdate { + update_type: "failed", + step_id: id, + extra_details: Some(error.clone()), + }, + ) + .map_err(|_| "Failed to emit status to frontend".to_string())?; + return Err(error); + } + + pub fn fail_if_err(&self, id: &str, res: Result) -> Result { + match res { + Ok(t) => Ok(t), + Err(e) => self.fail::(id, e), + } + } + + pub fn fail_if_err_map String>( + &self, + id: &str, + res: Result, + map_err: O, + ) -> Result { + match res { + Ok(t) => Ok(t), + Err(e) => { + let err = map_err(e); + self.fail::(id, err.clone()) + } + } + } +} diff --git a/src-tauri/src/sideloader/apple.rs b/src-tauri/src/sideloader/apple.rs index bb108a9..88c7825 100644 --- a/src-tauri/src/sideloader/apple.rs +++ b/src-tauri/src/sideloader/apple.rs @@ -224,7 +224,7 @@ pub async fn ensure_device_registered( .list_devices(DeveloperDeviceType::Ios, team) .await .map_err(|e| { - emit_error_and_return(window, &format!("Failed to list devices: {:?}", e)) + emit_error_and_return::<()>(window, &format!("Failed to list devices: {:?}", e)) .err() .unwrap() })?; diff --git a/src-tauri/src/sideloader/sideload.rs b/src-tauri/src/sideloader/sideload.rs index 58ae76a..f25920c 100644 --- a/src-tauri/src/sideloader/sideload.rs +++ b/src-tauri/src/sideloader/sideload.rs @@ -12,12 +12,12 @@ use std::{io::Write, path::PathBuf}; use tauri::{Emitter, Manager}; use tauri_plugin_shell::{process::CommandEvent, ShellExt}; -pub async fn sideload_ipa( +pub async fn sideload_app( handle: &tauri::AppHandle, window: tauri::Window, anisette_server: String, device: DeviceInfo, - ipa_path: PathBuf, + app_path: PathBuf, ) -> Result<(), String> { if device.uuid.is_empty() { return emit_error_and_return(&window, "No device selected"); @@ -71,7 +71,7 @@ pub async fn sideload_ipa( } }; - let mut app = crate::sideloader::application::Application::new(ipa_path); + let mut app = crate::sideloader::application::Application::new(app_path); let is_sidestore = app.bundle.bundle_identifier().unwrap_or("") == "com.SideStore.SideStore"; let main_app_bundle_id = match app.bundle.bundle_identifier() { Some(id) => id.to_string(), diff --git a/src-tauri/src/theos.rs b/src-tauri/src/theos.rs deleted file mode 100644 index 21cdc79..0000000 --- a/src-tauri/src/theos.rs +++ /dev/null @@ -1,325 +0,0 @@ -use std::io::{BufRead, BufReader}; -use std::process::{Command, Stdio}; -use std::thread; -use tauri::path::BaseDirectory; -use tauri::{Emitter, Manager}; - -use crate::device::DeviceInfo; -use crate::emit_error_and_return; -use crate::sideloader::sideload::sideload_ipa; - -pub async fn pipe_command( - cmd: &mut Command, - window: tauri::Window, - cmd_name: &str, - emit_exit_code: bool, -) -> Result<(), String> { - let name = &format!("{}-output", cmd_name); - cmd.stdout(Stdio::piped()); - cmd.stderr(Stdio::piped()); - - let mut command = match cmd.spawn() { - Ok(cmd) => cmd, - Err(_) => { - return emit_error_and_return(&window, "Failed to spawn build command"); - } - }; - - let stdout = match command.stdout.take() { - Some(out) => out, - None => { - return emit_error_and_return(&window, "Failed to get stdout"); - } - }; - - let stderr = match command.stderr.take() { - Some(err) => err, - None => { - return emit_error_and_return(&window, "Failed to get stderr"); - } - }; - - let stdout_handle = spawn_output_thread(stdout, window.clone(), name.to_string()); - let stderr_handle = spawn_output_thread(stderr, window.clone(), name.to_string()); - - stdout_handle.join().expect("stdout thread panicked"); - stderr_handle.join().expect("stderr thread panicked"); - - let exit_status = match command.wait() { - Ok(status) => status, - Err(_) => { - return emit_error_and_return(&window, "Failed to wait for command"); - } - }; - - let exit_code = exit_status.code().unwrap_or(1); - - if emit_exit_code { - window - .emit(name, format!("command.done.{}", exit_code)) - .expect("failed to send output"); - } - - if exit_code != 0 { - return Err(format!("Command exited with code {}", exit_code)); - } - - Ok(()) -} - -fn spawn_output_thread( - reader: R, - window: tauri::Window, - name: String, -) -> std::thread::JoinHandle<()> { - thread::spawn(move || { - let reader = BufReader::new(reader); - for line in reader.lines() { - match line { - Ok(line) => { - window.emit(&name, line).expect("failed to send output"); - } - Err(err) => { - window - .emit(&name, "command.done.999".to_string()) - .expect("failed to send output"); - eprintln!("Error reading output: {}", err); - return; - } - } - } - }) -} - -pub fn windows_to_wsl_path(path: &str) -> String { - let (drive_letter_index, rest_of_path_index) = if path.starts_with("\\\\?\\") { - (4, 6) - } else { - (0, 2) - }; - - let drive_letter = path[drive_letter_index..] - .chars() - .next() - .unwrap() - .to_ascii_lowercase(); - let rest_of_path = path[rest_of_path_index..].replace("\\", "/"); - format!("/mnt/{}/{}", drive_letter, rest_of_path) -} - -#[tauri::command] -pub async fn has_wsl() -> bool { - if !is_windows() { - return false; - } - - let output = Command::new("wsl") - .arg("echo") - .arg("1") - .stdout(Stdio::piped()) - .output() - .expect("failed to execute process"); - - let output = String::from_utf8_lossy(&output.stdout); - return output.trim() == "1"; -} - -#[tauri::command] -pub async fn has_theos() -> bool { - if is_windows() { - if !has_wsl().await { - return false; - } - - // For some reason, without cmd /C the command doesn't work properly. I'm guessing its some sort of quoting issue but I couldn't figure it out. - let output = Command::new("cmd") - .args(&["/C", r#"wsl bash -ic 'test -d $THEOS/extras ; echo $?'"#]) - .stdout(Stdio::piped()) - .output() - .expect("failed to execute process"); - - let stdout = String::from_utf8_lossy(&output.stdout); - - return stdout.trim() == "0"; - } - if let Ok(theos) = std::env::var("THEOS") { - let path = std::path::Path::new(&theos); - if path.exists() && path.is_dir() { - return true; - } - } - let home_dir = match std::env::var("HOME") { - Ok(home) => home, - Err(_) => return false, - }; - let theos_path = std::path::Path::new(&home_dir).join("theos"); - if theos_path.exists() && theos_path.is_dir() { - return true; - } - false -} - -#[tauri::command] -pub async fn install_theos_windows( - handle: tauri::AppHandle, - window: tauri::Window, - password: String, -) -> Result<(), String> { - let resource_path = match handle - .path() - .resolve("install_theos.sh", BaseDirectory::Resource) - { - Ok(path) => path, - Err(_) => { - return emit_error_and_return(&window, ""); - } - }; - - let wsl_path = windows_to_wsl_path(&resource_path.to_string_lossy()); - let mut command = Command::new("wsl"); - // Windows line endings are \r\n, so we need to remove the \r for bash to work properly - command.arg("sh").arg("-c").arg(format!( - "export SUDO_PASSWORD={} ; tr -d '\r' < {} | bash", - password, wsl_path - )); - - pipe_command(&mut command, window, "install-theos", true).await -} - -#[tauri::command] -pub async fn install_theos_linux( - handle: tauri::AppHandle, - window: tauri::Window, -) -> Result<(), String> { - let resource_path = match handle - .path() - .resolve("install_theos.sh", BaseDirectory::Resource) - { - Ok(path) => path, - Err(_) => { - return emit_error_and_return(&window, ""); - } - }; - - let mut command = Command::new("sh"); - command - .arg("-c") - .arg(format!("bash {}", resource_path.display())); - - pipe_command(&mut command, window, "install-theos", true).await -} - -#[tauri::command] -pub async fn update_theos(window: tauri::Window) -> Result<(), String> { - let mut command = if is_windows() { - let mut cmd = Command::new("wsl"); - cmd.arg("bash").arg("-ic").arg("'$THEOS/bin/update-theos'"); - cmd - } else { - let mut cmd = Command::new("sh"); - cmd.arg("-c").arg("$THEOS/bin/update-theos"); - cmd - }; - - pipe_command(&mut command, window, "update-theos", true).await -} - -pub async fn theos_cmd_linux( - window: tauri::Window, - folder: &str, - emit_exit_code: bool, - cmd: &str, -) -> Result<(), String> { - let mut command = Command::new("sh"); - command - .arg("-c") - .arg(format!("cd {} && make {}", folder, cmd)); - - pipe_command(&mut command, window, "build", emit_exit_code).await -} - -pub async fn theos_cmd_windows( - window: tauri::Window, - folder: &str, - emit_exit_code: bool, - cmd: &str, -) -> Result<(), String> { - let mut command = Command::new("wsl"); - command.arg("bash").arg("-ic").arg(format!( - "cd {} && make {}", - windows_to_wsl_path(folder), - cmd - )); - - pipe_command(&mut command, window, "build", emit_exit_code).await -} - -async fn theos_cmd( - window: tauri::Window, - folder: String, - emit_exit_code: bool, - cmd: &str, -) -> Result<(), String> { - if is_windows() { - return theos_cmd_windows(window, &folder, emit_exit_code, cmd).await; - } else { - return theos_cmd_linux(window, &folder, emit_exit_code, cmd).await; - } -} - -#[tauri::command] -pub async fn build_theos(window: tauri::Window, folder: String) -> Result<(), String> { - theos_cmd(window, folder, true, "package").await -} - -#[tauri::command] -pub async fn clean_theos(window: tauri::Window, folder: String) -> Result<(), String> { - theos_cmd(window, folder, true, "clean").await -} - -#[tauri::command] -pub async fn deploy_theos( - handle: tauri::AppHandle, - window: tauri::Window, - anisette_server: String, - device: DeviceInfo, - folder: String, -) -> Result<(), String> { - let packages_path = std::path::PathBuf::from(&folder).join("packages"); - // delete everything in the packages directory - if packages_path.exists() { - std::fs::remove_dir_all(&packages_path) - .map_err(|e| format!("Failed to remove packages directory: {}", e.to_string()))?; - } - - theos_cmd(window.clone(), folder.clone(), false, "package").await?; - window - .emit("build-output", "App Built Succesfully!".to_string()) - .ok(); - let ipa_path = std::fs::read_dir(&packages_path) - .unwrap() - .filter_map(Result::ok) - .find(|entry| entry.path().extension().map_or(false, |ext| ext == "ipa")) - .map(|entry| entry.path()); - - if ipa_path.is_none() { - return emit_error_and_return(&window, "No IPA file found in packages directory"); - } - let ipa_path = ipa_path.unwrap(); - - sideload_ipa(&handle, window.clone(), anisette_server, device, ipa_path).await?; - - window - .emit("build-output", "App installed!".to_string()) - .ok(); - window - .emit("build-output", "command.done.0".to_string()) - .ok(); - - Ok(()) -} - -#[tauri::command] -pub fn is_windows() -> bool { - cfg!(target_os = "windows") -} diff --git a/src-tauri/src/windows.rs b/src-tauri/src/windows.rs new file mode 100644 index 0000000..53ee3b9 --- /dev/null +++ b/src-tauri/src/windows.rs @@ -0,0 +1,39 @@ +use std::process::{Command, Stdio}; + +pub fn windows_to_wsl_path(path: &str) -> String { + let (drive_letter_index, rest_of_path_index) = if path.starts_with("\\\\?\\") { + (4, 6) + } else { + (0, 2) + }; + + let drive_letter = path[drive_letter_index..] + .chars() + .next() + .unwrap() + .to_ascii_lowercase(); + let rest_of_path = path[rest_of_path_index..].replace("\\", "/"); + format!("/mnt/{}/{}", drive_letter, rest_of_path) +} + +#[tauri::command] +pub async fn has_wsl() -> bool { + if !is_windows() { + return false; + } + + let output = Command::new("wsl") + .arg("echo") + .arg("1") + .stdout(Stdio::piped()) + .output() + .expect("failed to execute process"); + + let output = String::from_utf8_lossy(&output.stdout); + return output.trim() == "1"; +} + +#[tauri::command] +pub fn is_windows() -> bool { + cfg!(target_os = "windows") +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 8037fb8..a9a658d 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -8,7 +8,7 @@ "bundle": { "active": true, "targets": "all", - "resources": ["install_theos.sh", "templates"], + "resources": ["templates", "unxip"], "externalBin": ["zsign"], "icon": [ "icons/32x32.png", diff --git a/src-tauri/templates/uikit/Resources/Info.plist b/src-tauri/templates/swiftui/Info.plist similarity index 91% rename from src-tauri/templates/uikit/Resources/Info.plist rename to src-tauri/templates/swiftui/Info.plist index 013e692..70f12f3 100644 --- a/src-tauri/templates/uikit/Resources/Info.plist +++ b/src-tauri/templates/swiftui/Info.plist @@ -3,9 +3,9 @@ CFBundleName - {{projectName}} + [[product]] CFBundleExecutable - {{projectName}} + [[product]] CFBundleIcons CFBundlePrimaryIcon @@ -40,7 +40,7 @@ CFBundleIdentifier - {{bundleId}} + [[bundle_id]] CFBundleInfoDictionaryVersion 6.0 CFBundlePackageType @@ -52,7 +52,9 @@ iPhoneOS CFBundleVersion - 1.0 + [[version_num]] + CFBundleShortVersionString + [[version_string]] LSRequiresIPhoneOS UIDeviceFamily diff --git a/src-tauri/templates/swiftui/Makefile b/src-tauri/templates/swiftui/Makefile deleted file mode 100644 index 3d92ccb..0000000 --- a/src-tauri/templates/swiftui/Makefile +++ /dev/null @@ -1,12 +0,0 @@ -TARGET = iphone:clang:latest:16.5 -INSTALL_TARGET_PROCESSES = {{projectName}} -PACKAGE_FORMAT=ipa -ARCHS = arm64 - -include $(THEOS)/makefiles/common.mk - -APPLICATION_NAME = {{projectName}} - -{{projectName}}_FILES = $(shell find src -name '*.swift' | grep -v '/Package.swift$$') - -include $(THEOS_MAKE_PATH)/application.mk diff --git a/src-tauri/templates/swiftui/Package.swift b/src-tauri/templates/swiftui/Package.swift new file mode 100644 index 0000000..6823e78 --- /dev/null +++ b/src-tauri/templates/swiftui/Package.swift @@ -0,0 +1,14 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "{{projectName}}", + platforms: [.iOS(.v15)], + targets: [ + .executableTarget( + name: "{{projectName}}", + path: "src" + ), + ] +) diff --git a/src-tauri/templates/swiftui/control b/src-tauri/templates/swiftui/control deleted file mode 100644 index 9ab4d83..0000000 --- a/src-tauri/templates/swiftui/control +++ /dev/null @@ -1,9 +0,0 @@ -Package: {{bundleId}} -Name: {{projectName}} -Version: 0.0.1 -Architecture: iphoneos-arm -Description: {{projectDescription}} -Maintainer: {{author}} -Author: {{author}} -Section: Utilities -Depends: firmware (>= 14.0) diff --git a/src-tauri/templates/swiftui/ycode.toml b/src-tauri/templates/swiftui/ycode.toml new file mode 100644 index 0000000..b2c720a --- /dev/null +++ b/src-tauri/templates/swiftui/ycode.toml @@ -0,0 +1,6 @@ +format_version = 1 + +[project] +version_num = "1" +version_string = "1.0.0" +bundle_id = "{{bundleId}}" \ No newline at end of file diff --git a/src-tauri/templates/swiftui/Resources/Info.plist b/src-tauri/templates/uikit/Info.plist similarity index 91% rename from src-tauri/templates/swiftui/Resources/Info.plist rename to src-tauri/templates/uikit/Info.plist index 013e692..70f12f3 100644 --- a/src-tauri/templates/swiftui/Resources/Info.plist +++ b/src-tauri/templates/uikit/Info.plist @@ -3,9 +3,9 @@ CFBundleName - {{projectName}} + [[product]] CFBundleExecutable - {{projectName}} + [[product]] CFBundleIcons CFBundlePrimaryIcon @@ -40,7 +40,7 @@ CFBundleIdentifier - {{bundleId}} + [[bundle_id]] CFBundleInfoDictionaryVersion 6.0 CFBundlePackageType @@ -52,7 +52,9 @@ iPhoneOS CFBundleVersion - 1.0 + [[version_num]] + CFBundleShortVersionString + [[version_string]] LSRequiresIPhoneOS UIDeviceFamily diff --git a/src-tauri/templates/uikit/Makefile b/src-tauri/templates/uikit/Makefile deleted file mode 100644 index 3d92ccb..0000000 --- a/src-tauri/templates/uikit/Makefile +++ /dev/null @@ -1,12 +0,0 @@ -TARGET = iphone:clang:latest:16.5 -INSTALL_TARGET_PROCESSES = {{projectName}} -PACKAGE_FORMAT=ipa -ARCHS = arm64 - -include $(THEOS)/makefiles/common.mk - -APPLICATION_NAME = {{projectName}} - -{{projectName}}_FILES = $(shell find src -name '*.swift' | grep -v '/Package.swift$$') - -include $(THEOS_MAKE_PATH)/application.mk diff --git a/src-tauri/templates/uikit/Package.swift b/src-tauri/templates/uikit/Package.swift new file mode 100644 index 0000000..6823e78 --- /dev/null +++ b/src-tauri/templates/uikit/Package.swift @@ -0,0 +1,14 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "{{projectName}}", + platforms: [.iOS(.v15)], + targets: [ + .executableTarget( + name: "{{projectName}}", + path: "src" + ), + ] +) diff --git a/src-tauri/templates/uikit/control b/src-tauri/templates/uikit/control deleted file mode 100644 index 9ab4d83..0000000 --- a/src-tauri/templates/uikit/control +++ /dev/null @@ -1,9 +0,0 @@ -Package: {{bundleId}} -Name: {{projectName}} -Version: 0.0.1 -Architecture: iphoneos-arm -Description: {{projectDescription}} -Maintainer: {{author}} -Author: {{author}} -Section: Utilities -Depends: firmware (>= 14.0) diff --git a/src-tauri/templates/uikit/ycode.toml b/src-tauri/templates/uikit/ycode.toml new file mode 100644 index 0000000..b2c720a --- /dev/null +++ b/src-tauri/templates/uikit/ycode.toml @@ -0,0 +1,6 @@ +format_version = 1 + +[project] +version_num = "1" +version_string = "1.0.0" +bundle_id = "{{bundleId}}" \ No newline at end of file diff --git a/src-tauri/unxip b/src-tauri/unxip new file mode 100755 index 0000000..a2069ee Binary files /dev/null and b/src-tauri/unxip differ diff --git a/src/App.tsx b/src/App.tsx index bfe79d1..fae46e6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -81,9 +81,9 @@ const App = () => { } /> } /> } /> - - } /> + } /> + diff --git a/src/components/CodeEditor.tsx b/src/components/CodeEditor.tsx index 03f993d..faa905c 100644 --- a/src/components/CodeEditor.tsx +++ b/src/components/CodeEditor.tsx @@ -42,6 +42,7 @@ const getLanguage = async (filename: string) => { xm: "objective-c", xmi: "objective-c", sh: "shell", + toml: "toml", }; return extToLang[ext] || "plaintext"; }; diff --git a/src/components/Menu/MenuBar.tsx b/src/components/Menu/MenuBar.tsx index 5ab30b2..043847d 100644 --- a/src/components/Menu/MenuBar.tsx +++ b/src/components/Menu/MenuBar.tsx @@ -183,7 +183,7 @@ export default function MenuBar({ callbacks }: MenuBarProps) { const resetMenuIndex = useCallback(() => setMenuIndex(null), []); const { path } = useParams<"path">(); - const { devices } = useIDE(); + const { devices, selectedToolchain } = useIDE(); const [anisetteServer] = useStore( "apple-id/anisette-server", "ani.sidestore.io" @@ -344,16 +344,23 @@ export default function MenuBar({ callbacks }: MenuBarProps) { ))} } - parameters={{ folder: path }} + parameters={{ + folder: path, + toolchainPath: selectedToolchain?.path ?? "", + }} sx={{ marginLeft: "auto", marginRight: 0 }} /> } - parameters={{ folder: path }} + parameters={{ + folder: path, + toolchainPath: selectedToolchain?.path ?? "", + debug: true, + }} sx={{ marginRight: 0 }} /> @@ -389,12 +396,14 @@ export default function MenuBar({ callbacks }: MenuBarProps) { } parameters={{ folder: path, anisetteServer, device: selectedDevice, + toolchainPath: selectedToolchain?.path ?? "", + debug: true, }} validate={() => { if (!selectedDevice) { diff --git a/src/components/OperationView.css b/src/components/OperationView.css new file mode 100644 index 0000000..7146ddc --- /dev/null +++ b/src/components/OperationView.css @@ -0,0 +1,32 @@ +.operation-content { + display: flex; + flex-direction: column; + gap: var(--padding-xl); +} + +.operation-step-icon { + width: 1.5rem; + height: 1.5rem; + min-width: 1.5rem; + min-height: 1.5rem; + + font-size: 1.5rem; +} + +.operation-step { + display: flex; + gap: var(--padding-md); + align-items: center; +} + +.operation-extra-details { + background-color: black; + overflow-x: auto; + padding: var(--padding-md); + border-radius: 1px; + margin: 0; +} + +.operation-step-internal { + flex-shrink: 1; +} diff --git a/src/components/OperationView.tsx b/src/components/OperationView.tsx new file mode 100644 index 0000000..24bdbdb --- /dev/null +++ b/src/components/OperationView.tsx @@ -0,0 +1,109 @@ +import { + Accordion, + AccordionDetails, + AccordionSummary, + Divider, + Modal, + ModalClose, + ModalDialog, + Typography, +} from "@mui/joy"; +import { OperationState } from "../utilities/operations"; +import "./OperationView.css"; +import { SuccessIcon, ErrorIcon, StyledLoadingIcon } from "react-toast-plus"; +import { PanoramaFishEye, DoNotDisturbOn } from "@mui/icons-material"; + +export default ({ + operationState, + closeMenu, +}: { + operationState: OperationState; + closeMenu: () => void; +}) => { + const operation = operationState.current; + const opFailed = operationState.failed.length > 0; + const done = + opFailed || operationState.completed.length == operation.steps.length; + + return ( + { + if (done) closeMenu(); + }} + > + + {done && } +
+ {operation?.title} + + {done + ? opFailed + ? "Operation failed. Please see steps for details." + : "Operation completed!" + : "Please wait..."} + +
+ +
+ {operation.steps.map((step) => { + let failed = operationState.failed.find((f) => f.stepId == step.id); + let completed = operationState.completed.includes(step.id); + let started = operationState.started.includes(step.id); + let notStarted = !failed && !completed && !started; + return ( +
+
+ {failed && } + {!failed && completed && } + {!failed && !completed && started && } + {notStarted && !opFailed && ( + + )} + {notStarted && opFailed && ( + + )} +
+ +
+ + {step.title} + + {failed && ( + + + Show Details + + +
+                          {failed.extraDetails}
+                        
+
+
+ )} +
+
+ ); + })} +
+
+
+ ); +}; diff --git a/src/components/RunCommand.css b/src/components/RunCommand.css deleted file mode 100644 index b2e025f..0000000 --- a/src/components/RunCommand.css +++ /dev/null @@ -1,7 +0,0 @@ -.console { - overflow-x: auto; - background-color: #000; - border-radius: 15px; - padding: 0 var(--padding-md); - min-width: 80vw; -} diff --git a/src/components/RunCommand.tsx b/src/components/RunCommand.tsx deleted file mode 100644 index ad2f4ce..0000000 --- a/src/components/RunCommand.tsx +++ /dev/null @@ -1,181 +0,0 @@ -// Create a simple component to display text in a console-style in a mui joy modal. - -import { - Button, - Input, - Modal, - ModalClose, - ModalDialog, - Typography, -} from "@mui/joy"; -import { listen } from "@tauri-apps/api/event"; -import { useEffect, useRef, useState } from "react"; -import Convert from "ansi-to-html"; -import "./RunCommand.css"; -import { invoke } from "@tauri-apps/api/core"; - -const convert = new Convert(); - -interface RunCommandProps { - title: string; - failedMessage?: string; - doneMessage?: string; - command: string; - listener: string; - run: boolean; - setRun: (run: boolean) => void; - askPassword?: boolean; -} - -export default ({ - title, - command, - listener, - run, - setRun, - failedMessage, - doneMessage, - askPassword, -}: RunCommandProps) => { - const [open, setOpen] = useState(false); - const [passwordOpen, setPasswordOpen] = useState(false); - const [password, setPassword] = useState(""); - - const [body, setBody] = useState(""); - const [html, setHtml] = useState(""); - const [status, setStatus] = useState("none"); - - const preRef = useRef(null); - const listenerAdded = useRef(false); - const hasRun = useRef(false); - const passwordAsked = useRef(false); - - function startCommand() { - if (hasRun.current) return; - setOpen(true); - setStatus("running"); - if (askPassword) { - invoke(command, { password }); - setPassword(""); - } else { - invoke(command); - } - hasRun.current = true; - } - useEffect(() => { - if (run && askPassword && !passwordAsked.current) { - setPasswordOpen(true); - passwordAsked.current = true; - } else if (run && !hasRun.current) { - startCommand(); - } - }, [run]); - - useEffect(() => { - if (!listenerAdded.current) { - listen(listener, (event) => { - let line = event.payload as string; - if (line.includes("command.done")) { - if (line.split(".")[2] !== "0") { - setStatus("failed"); - return; - } - setStatus("done"); - return; - } - setBody((body) => body + line + "\n"); - }); - listenerAdded.current = true; - } - }, []); - - useEffect(() => { - if (open) { - if (body.startsWith("\n")) { - setHtml(convert.toHtml(body.slice(1))); - } else { - setHtml(convert.toHtml(body)); - } - } - }, [body]); - - useEffect(() => { - if (preRef.current) { - const element = preRef.current; - setTimeout(() => { - element.scrollIntoView({ behavior: "smooth", block: "end" }); - }, 0); - } - }, [html]); - - return ( - <> - { - setPasswordOpen(false); - setPassword(""); - passwordAsked.current = false; - }} - > - - - Enter your WSL sudo password. It will not be saved. - -
{ - setPasswordOpen(false); - startCommand(); - }} - > - setPassword(e.target.value)} - /> - -
-
-
- { - setOpen(false); - setRun(false); - setBody(""); - setHtml(""); - setStatus("none"); - hasRun.current = false; - passwordAsked.current = false; - } - : () => {} - } - > - - {(status === "done" || status === "failed") && } - - {status === "failed" - ? failedMessage ?? "Failed" - : status === "done" - ? doneMessage ?? "Done" - : title} - -
-

-          
-
-
- - ); -}; diff --git a/src/components/SDKMenu.tsx b/src/components/SDKMenu.tsx new file mode 100644 index 0000000..bc29302 --- /dev/null +++ b/src/components/SDKMenu.tsx @@ -0,0 +1,85 @@ +import { Button, Typography } from "@mui/joy"; +import { useIDE } from "../utilities/IDEContext"; +import { open } from "@tauri-apps/plugin-dialog"; +import { useToast } from "react-toast-plus"; +import { useCallback, useEffect } from "react"; +import { openUrl } from "@tauri-apps/plugin-opener"; +import { installSdkOperation } from "../utilities/operations"; + +export default () => { + const { selectedToolchain, hasDarwinSDK, checkSDK, startOperation } = + useIDE(); + const { addToast } = useToast(); + + const install = useCallback(async () => { + let xipPath = await open({ + directory: false, + multiple: false, + filters: [ + { + name: "XCode", + extensions: ["xip"], + }, + ], + }); + if (!xipPath) { + addToast.error("No Xcode.xip selected"); + return; + } + const params = { + xcodePath: xipPath, + toolchainPath: selectedToolchain?.path || "", + }; + await startOperation(installSdkOperation, params); + checkSDK(); + }, [selectedToolchain, addToast]); + + useEffect(() => { + checkSDK(); + }, [checkSDK]); + + if (hasDarwinSDK === null) { + return
Checking for SDK...
; + } + + return ( +
+ + {hasDarwinSDK + ? "Darwin SDK is installed!" + : "Darwin SDK is not installed."} + +
+ + + +
+
+ ); +}; diff --git a/src/components/SwiftMenu.tsx b/src/components/SwiftMenu.tsx new file mode 100644 index 0000000..149a49d --- /dev/null +++ b/src/components/SwiftMenu.tsx @@ -0,0 +1,150 @@ +import { Button, FormControl, Radio, RadioGroup, Typography } from "@mui/joy"; +import { Toolchain, useIDE } from "../utilities/IDEContext"; +import { useMemo } from "react"; +import { openUrl } from "@tauri-apps/plugin-opener"; + +export default () => { + const { + selectedToolchain, + setSelectedToolchain, + toolchains, + scanToolchains, + locateToolchain, + } = useIDE(); + + const allToolchains = useMemo(() => { + let all: Toolchain[] = []; + if (toolchains !== null && toolchains.toolchains) { + all = [...toolchains.toolchains]; + } + if ( + selectedToolchain && + !all.some( + (t) => stringifyToolchain(t) === stringifyToolchain(selectedToolchain) + ) + ) { + all.push(selectedToolchain); + } + return all; + }, [selectedToolchain, toolchains]); + return ( +
+ + {toolchains === null + ? "Checking for Swift..." + : toolchains.swiftlyInstalled + ? `Swiftly Detected: ${toolchains.swiftlyVersion}` + : "YCode was unable to detect Swiftly."} + + {toolchains !== null && allToolchains.length === 0 && ( + + No Swift toolchains found. You can install one using " + + swiftly install latest + + " or manually. + + )} + {toolchains !== null && allToolchains.length > 0 && ( +
+ Select a toolchain: + + {allToolchains.map((toolchain) => ( + + setSelectedToolchain(toolchain)} + /> +
+ {toolchain.path} + + {toolchain.isSwiftly ? "(Swiftly)" : "(Manually Installed)"} + +
+
+ ))} +
+
+ )} +
+ { + + } + {toolchains?.swiftlyInstalled === false && + selectedToolchain === null && ( + + )} + { + + } +
+
+ ); +}; + +function isCompatable(toolchain: Toolchain | null): boolean { + if (!toolchain) return false; + return toolchain.version.startsWith("6.0"); +} + +function stringifyToolchain(toolchain: Toolchain | null): string | null { + if (!toolchain) return null; + return `${toolchain.path}:${toolchain.version}:${toolchain.isSwiftly}`; +} diff --git a/src/pages/Onboarding.tsx b/src/pages/Onboarding.tsx index cbb88c2..dcfbdc0 100644 --- a/src/pages/Onboarding.tsx +++ b/src/pages/Onboarding.tsx @@ -2,27 +2,28 @@ import { useEffect, useState } from "react"; import { open } from "@tauri-apps/plugin-shell"; import "./Onboarding.css"; import { Button, Card, CardContent, Divider, Link, Typography } from "@mui/joy"; -import RunCommand from "../components/RunCommand"; import { useIDE } from "../utilities/IDEContext"; import logo from "../assets/logo.png"; import { useNavigate } from "react-router"; +import { openUrl } from "@tauri-apps/plugin-opener"; +import SwiftMenu from "../components/SwiftMenu"; +import SDKMenu from "../components/SDKMenu"; export interface OnboardingProps {} export default ({}: OnboardingProps) => { - const { hasTheos, hasWSL, isWindows, openFolderDialog } = useIDE(); + const { selectedToolchain, toolchains, hasWSL, isWindows, openFolderDialog } = + useIDE(); const [ready, setReady] = useState(false); - const [updatingTheos, setUpdatingTheos] = useState(false); - const [installingTheos, setInstallingTheos] = useState(false); const navigate = useNavigate(); useEffect(() => { - if (hasTheos !== null && isWindows !== null && hasWSL !== null) { - setReady(hasTheos && (isWindows ? hasWSL : true)); + if (toolchains !== null && isWindows !== null && hasWSL !== null) { + setReady(selectedToolchain !== null && (isWindows ? hasWSL : true)); } else { setReady(false); } - }, [hasTheos, hasWSL, isWindows]); + }, [selectedToolchain, toolchains, hasWSL, isWindows]); return (
@@ -58,7 +59,7 @@ export default ({}: OnboardingProps) => { - )} - {hasTheos === true && ( - - )} - {hasTheos !== null && (!isWindows || hasWSL) && ( - - )} - -
+ + + + + Darwin SDK + + YCode requires a special swift SDK to build apps for iOS. It can be + generated from a copy of Xcode 16 or later. To install it, download + Xcode.xip using the link below, click the "Install SDK" button, then + select the downloaded file. + + + + diff --git a/src/preferences/Prefs.css b/src/preferences/Prefs.css index 44ed689..4403c0a 100644 --- a/src/preferences/Prefs.css +++ b/src/preferences/Prefs.css @@ -73,6 +73,7 @@ gap: var(--padding-sm); padding-left: var(--padding-md); overflow: auto; + flex-grow: 1; } diff --git a/src/preferences/pages/index.ts b/src/preferences/pages/index.ts index 217b8ea..ff74a62 100644 --- a/src/preferences/pages/index.ts +++ b/src/preferences/pages/index.ts @@ -7,6 +7,7 @@ import { appleIdPage } from "./appleId"; import { certificatesPage } from "./certificates"; import { appIdsPage } from "./appIds"; import { developerPage } from "./developer"; +import { swiftPage } from "./swift"; const generalCategory: PreferenceCategory = { id: "general", @@ -20,6 +21,12 @@ const appleCategory: PreferenceCategory = { pages: [appleIdPage, certificatesPage, appIdsPage], }; +const swiftCategory: PreferenceCategory = { + id: "swift", + name: "Swift", + pages: [swiftPage], +}; + const advancedCategory: PreferenceCategory = { id: "advanced", name: "Advanced", @@ -28,6 +35,7 @@ const advancedCategory: PreferenceCategory = { preferenceRegistry.registerCategory(generalCategory); preferenceRegistry.registerCategory(appleCategory); +preferenceRegistry.registerCategory(swiftCategory); preferenceRegistry.registerCategory(advancedCategory); export { preferenceRegistry }; diff --git a/src/preferences/pages/swift.tsx b/src/preferences/pages/swift.tsx new file mode 100644 index 0000000..07f5abc --- /dev/null +++ b/src/preferences/pages/swift.tsx @@ -0,0 +1,26 @@ +import { Divider } from "@mui/joy"; +import SDKMenu from "../../components/SDKMenu"; +import SwiftMenu from "../../components/SwiftMenu"; +import { createCustomPreferencePage } from "../helpers"; + +export const swiftPage = createCustomPreferencePage( + "swift", + "Swift", + () => ( +
+ + + +
+ ), + { + description: "Manage your swift toolchains and Darwin SDK", + category: "swift", + } +); diff --git a/src/utilities/IDEContext.tsx b/src/utilities/IDEContext.tsx index 21f0520..a09174d 100644 --- a/src/utilities/IDEContext.tsx +++ b/src/utilities/IDEContext.tsx @@ -22,16 +22,31 @@ import { Typography, } from "@mui/joy"; import { useCommandRunner } from "./Command"; +import { useStore } from "./StoreContext"; +import { Operation, OperationState, OperationUpdate } from "./operations"; +import OperationView from "../components/OperationView"; export interface IDEContextType { initialized: boolean; isWindows: boolean; hasWSL: boolean; - hasTheos: boolean; + hasDarwinSDK: boolean; + toolchains: ListToolchainResponse | null; + selectedToolchain: Toolchain | null; devices: DeviceInfo[]; openFolderDialog: () => void; consoleLines: string[]; setConsoleLines: React.Dispatch>; + scanToolchains: () => Promise; + checkSDK: () => Promise; + locateToolchain: () => Promise; + startOperation: ( + operation: Operation, + params: { [key: string]: any } + ) => Promise; + setSelectedToolchain: ( + value: Toolchain | ((oldValue: Toolchain | null) => Toolchain | null) | null + ) => void; } export type DeviceInfo = { @@ -40,6 +55,28 @@ export type DeviceInfo = { uuid: string; }; +export type Toolchain = { + version: string; + path: string; + isSwiftly: boolean; +}; + +type ListToolchainResponseWithSwiftly = { + swiftlyInstalled: true; + swiftlyVersion: string; + toolchains: Toolchain[]; +}; + +type ListToolchainResponseWithoutSwiftly = { + swiftlyInstalled: false; + swiftlyVersion: null; + toolchains: Toolchain[]; +}; + +export type ListToolchainResponse = + | ListToolchainResponseWithSwiftly + | ListToolchainResponseWithoutSwiftly; + export const IDEContext = createContext(null); export const IDEProvider: React.FC<{ @@ -47,18 +84,72 @@ export const IDEProvider: React.FC<{ }> = ({ children }) => { const [isWindows, setIsWindows] = useState(false); const [hasWSL, setHasWSL] = useState(false); - const [hasTheos, setHasTheos] = useState(false); + const [toolchains, setToolchains] = useState( + null + ); + const [hasDarwinSDK, setHasDarwinSDK] = useState(false); const [initialized, setInitialized] = useState(false); const [devices, setDevices] = useState([]); const [consoleLines, setConsoleLines] = useState([]); + const [selectedToolchain, setSelectedToolchain] = useStore( + "swift/selected-toolchain", + null + ); + + const checkSDK = useCallback(async () => { + try { + let result = await invoke("has_darwin_sdk", { + toolchainPath: selectedToolchain?.path || "", + }); + setHasDarwinSDK(result); + } catch (e) { + console.error("Failed to check for SDK:", e); + setHasDarwinSDK(false); + } + }, [selectedToolchain]); + + const scanToolchains = useCallback(() => { + return invoke("get_swiftly_toolchains").then( + (response) => { + if (response) { + setToolchains(response); + } + } + ); + }, []); + + const locateToolchain = useCallback(async () => { + const path = await dialog.open({ + directory: true, + multiple: false, + }); + if (!path) { + addToast.error("No path selected"); + return; + } + if (await invoke("validate_toolchain", { toolchainPath: path })) { + const info = await invoke("get_toolchain_info", { + toolchainPath: path, + }).catch((error) => { + console.error("Error getting toolchain info:", error); + addToast.error("Failed to get toolchain info"); + return null; + }); + if (!info) { + addToast.error("Invalid toolchain path or version not found"); + return; + } + if (info) { + setSelectedToolchain(info); + } + } else { + addToast.error("Invalid toolchain path"); + } + }, []); useEffect(() => { let initPromises: Promise[] = []; - initPromises.push( - invoke("has_theos").then((response) => { - setHasTheos(response as boolean); - }) - ); + initPromises.push(scanToolchains()); initPromises.push( invoke("has_wsl").then((response) => { setHasWSL(response as boolean); @@ -69,16 +160,21 @@ export const IDEProvider: React.FC<{ setIsWindows(response as boolean); }) ); + initPromises.push( + invoke("has_darwin_sdk", { + toolchainPath: selectedToolchain?.path ?? "", + }).then((response) => { + setHasDarwinSDK(response as boolean); + }) + ); Promise.all(initPromises) .then(() => { setInitialized(true); }) .catch((error) => { - console.error("Error initializing IDE context:", error); - alert( - "An error occurred while initializing the IDE context. Please check the console for details." - ); + console.error("Error initializing IDE context: ", error); + alert("An error occurred while initializing the IDE context: " + error); }); }, []); @@ -177,25 +273,100 @@ export const IDEProvider: React.FC<{ const { cancelCommand } = useCommandRunner(); + const [operationState, setOperationState] = useState( + null + ); + + const startOperation = useCallback( + async ( + operation: Operation, + params: { [key: string]: any } + ): Promise => { + setOperationState({ + current: operation, + started: [], + failed: [], + completed: [], + }); + return new Promise(async (resolve, reject) => { + const unlistenFn = await listen( + "operation_" + operation.id, + (event) => { + setOperationState((old) => { + if (old == null) return null; + if (event.payload.updateType === "started") { + return { + ...old, + started: [...old.started, event.payload.stepId], + }; + } else if (event.payload.updateType === "finished") { + return { + ...old, + completed: [...old.completed, event.payload.stepId], + }; + } else if (event.payload.updateType === "failed") { + return { + ...old, + failed: [ + ...old.failed, + { + stepId: event.payload.stepId, + extraDetails: event.payload.extraDetails, + }, + ], + }; + } + return old; + }); + } + ); + try { + await invoke(operation.id + "_operation", params); + unlistenFn(); + resolve(); + } catch (e) { + unlistenFn(); + reject(e); + } + }); + }, + [setOperationState] + ); + const contextValue = useMemo( () => ({ isWindows, hasWSL, - hasTheos, + toolchains, initialized, devices, openFolderDialog, consoleLines, setConsoleLines, + selectedToolchain, + scanToolchains, + locateToolchain, + setSelectedToolchain, + hasDarwinSDK, + checkSDK, + startOperation, }), [ isWindows, hasWSL, - hasTheos, + toolchains, initialized, devices, openFolderDialog, consoleLines, + setConsoleLines, + selectedToolchain, + scanToolchains, + locateToolchain, + setSelectedToolchain, + hasDarwinSDK, + checkSDK, + startOperation, ] ); @@ -330,6 +501,14 @@ export const IDEProvider: React.FC<{ + {operationState && ( + { + setOperationState(null); + }} + /> + )} ); }; diff --git a/src/utilities/operations.ts b/src/utilities/operations.ts new file mode 100644 index 0000000..6296549 --- /dev/null +++ b/src/utilities/operations.ts @@ -0,0 +1,68 @@ +export type Operation = { + id: string; + title: string; + steps: OperationStep[]; +}; + +export type OperationStep = { + id: string; + title: string; +}; + +export type OperationState = { + current: Operation; + completed: string[]; + started: string[]; + failed: { + stepId: string; + extraDetails: string; + }[]; +}; + +type OperationInfoUpdate = { + updateType: "started" | "finished"; + stepId: string; +}; + +type OperationFailedUpdate = { + updateType: "failed"; + stepId: string; + extraDetails: string; +}; + +export type OperationUpdate = OperationInfoUpdate | OperationFailedUpdate; + +export const installSdkOperation: Operation = { + id: "install_sdk", + title: "Installing Darwin SDK", + steps: [ + { + id: "create_stage", + title: "Create Stage", + }, + { + id: "install_toolset", + title: "Download & Install toolset", + }, + { + id: "extract_xip", + title: "Extract Xcode.xip", + }, + { + id: "copy_files", + title: "Copy Files", + }, + { + id: "write_metadata", + title: "Write Metadata", + }, + { + id: "install_sdk", + title: "Install SDK", + }, + { + id: "cleanup", + title: "Clean Up", + }, + ], +}; diff --git a/src/utilities/templates.ts b/src/utilities/templates.ts index a04bbc0..a0b18ce 100644 --- a/src/utilities/templates.ts +++ b/src/utilities/templates.ts @@ -29,16 +29,6 @@ const defaultFields = { label: "Bundle Identifier", default: "com.example.myproject", }, - projectDescription: { - type: "text", - label: "Project Description", - default: "An awesome application", - }, - author: { - type: "text", - label: "Author", - default: "Steve Jobs", - }, }; export const templates: Template[] = [