diff --git a/.direnv/bin/nix-direnv-reload b/.direnv/bin/nix-direnv-reload new file mode 100755 index 0000000..0179b6f --- /dev/null +++ b/.direnv/bin/nix-direnv-reload @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -e +if [[ ! -d "/Users/eric/Projects/wails_tools" ]]; then + echo "Cannot find source directory; Did you move it?" + echo "(Looking for "/Users/eric/Projects/wails_tools")" + echo 'Cannot force reload with this script - use "direnv reload" manually and then try again' + exit 1 +fi + +# rebuild the cache forcefully +_nix_direnv_force_reload=1 direnv exec "/Users/eric/Projects/wails_tools" true + +# Update the mtime for .envrc. +# This will cause direnv to reload again - but without re-building. +touch "/Users/eric/Projects/wails_tools/.envrc" + +# Also update the timestamp of whatever profile_rc we have. +# This makes sure that we know we are up to date. +touch -r "/Users/eric/Projects/wails_tools/.envrc" "/Users/eric/Projects/wails_tools/.direnv"/*.rc diff --git a/.direnv/flake-inputs/7f0478ddr51i3r708dpkljnvmzwc2fhn-source b/.direnv/flake-inputs/7f0478ddr51i3r708dpkljnvmzwc2fhn-source new file mode 120000 index 0000000..991c1bf --- /dev/null +++ b/.direnv/flake-inputs/7f0478ddr51i3r708dpkljnvmzwc2fhn-source @@ -0,0 +1 @@ +/nix/store/7f0478ddr51i3r708dpkljnvmzwc2fhn-source \ No newline at end of file diff --git a/.direnv/flake-inputs/affmc6lhad8f6q3iaa3iydcdjwr8lwgp-source b/.direnv/flake-inputs/affmc6lhad8f6q3iaa3iydcdjwr8lwgp-source new file mode 120000 index 0000000..1fbc782 --- /dev/null +++ b/.direnv/flake-inputs/affmc6lhad8f6q3iaa3iydcdjwr8lwgp-source @@ -0,0 +1 @@ +/nix/store/affmc6lhad8f6q3iaa3iydcdjwr8lwgp-source \ No newline at end of file diff --git a/.direnv/flake-inputs/db9s997yqjazz8bapbxpr4r4h3m6rf53-source b/.direnv/flake-inputs/db9s997yqjazz8bapbxpr4r4h3m6rf53-source new file mode 120000 index 0000000..848a904 --- /dev/null +++ b/.direnv/flake-inputs/db9s997yqjazz8bapbxpr4r4h3m6rf53-source @@ -0,0 +1 @@ +/nix/store/db9s997yqjazz8bapbxpr4r4h3m6rf53-source \ No newline at end of file diff --git a/.direnv/flake-inputs/g5v3sgqy6a0fsmas7mnapc196flrplix-source b/.direnv/flake-inputs/g5v3sgqy6a0fsmas7mnapc196flrplix-source new file mode 120000 index 0000000..c425a39 --- /dev/null +++ b/.direnv/flake-inputs/g5v3sgqy6a0fsmas7mnapc196flrplix-source @@ -0,0 +1 @@ +/nix/store/g5v3sgqy6a0fsmas7mnapc196flrplix-source \ No newline at end of file diff --git a/.direnv/flake-inputs/jzfmmjnq1cip816awnliw7ir69pcyg00-source b/.direnv/flake-inputs/jzfmmjnq1cip816awnliw7ir69pcyg00-source new file mode 120000 index 0000000..1966127 --- /dev/null +++ b/.direnv/flake-inputs/jzfmmjnq1cip816awnliw7ir69pcyg00-source @@ -0,0 +1 @@ +/nix/store/jzfmmjnq1cip816awnliw7ir69pcyg00-source \ No newline at end of file diff --git a/.direnv/flake-inputs/kr977lds5rwp4n6c91fkr1sabzpm61wn-source b/.direnv/flake-inputs/kr977lds5rwp4n6c91fkr1sabzpm61wn-source new file mode 120000 index 0000000..9048f6c --- /dev/null +++ b/.direnv/flake-inputs/kr977lds5rwp4n6c91fkr1sabzpm61wn-source @@ -0,0 +1 @@ +/nix/store/kr977lds5rwp4n6c91fkr1sabzpm61wn-source \ No newline at end of file diff --git a/.direnv/flake-inputs/kx00h535s3jzb9803vnylxllij3zhix5-source b/.direnv/flake-inputs/kx00h535s3jzb9803vnylxllij3zhix5-source new file mode 120000 index 0000000..b328387 --- /dev/null +++ b/.direnv/flake-inputs/kx00h535s3jzb9803vnylxllij3zhix5-source @@ -0,0 +1 @@ +/nix/store/kx00h535s3jzb9803vnylxllij3zhix5-source \ No newline at end of file diff --git a/.direnv/flake-inputs/ngdfag0pfs1h54pbjs9ywah4zhqsphf1-source b/.direnv/flake-inputs/ngdfag0pfs1h54pbjs9ywah4zhqsphf1-source new file mode 120000 index 0000000..d9ddd38 --- /dev/null +++ b/.direnv/flake-inputs/ngdfag0pfs1h54pbjs9ywah4zhqsphf1-source @@ -0,0 +1 @@ +/nix/store/ngdfag0pfs1h54pbjs9ywah4zhqsphf1-source \ No newline at end of file diff --git a/.direnv/flake-inputs/nk13680f34w3q01a1q69c48my6fi7cxz-source b/.direnv/flake-inputs/nk13680f34w3q01a1q69c48my6fi7cxz-source new file mode 120000 index 0000000..c335fea --- /dev/null +++ b/.direnv/flake-inputs/nk13680f34w3q01a1q69c48my6fi7cxz-source @@ -0,0 +1 @@ +/nix/store/nk13680f34w3q01a1q69c48my6fi7cxz-source \ No newline at end of file diff --git a/.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa b/.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa new file mode 120000 index 0000000..ab25a30 --- /dev/null +++ b/.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa @@ -0,0 +1 @@ +/nix/store/8r540i4dsh49c6f8k9cfcmbwbwppx7dz-nix-shell-env \ No newline at end of file diff --git a/.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa.rc b/.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa.rc new file mode 100644 index 0000000..9744af5 --- /dev/null +++ b/.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa.rc @@ -0,0 +1,2417 @@ +unset shellHook +PATH=${PATH:-} +nix_saved_PATH="$PATH" +XDG_DATA_DIRS=${XDG_DATA_DIRS:-} +nix_saved_XDG_DATA_DIRS="$XDG_DATA_DIRS" +AR='ar' +export AR +AS='as' +export AS +BASH='/nix/store/3zrx6av2d1141igkcn8477cvbfqpcmcf-bash-5.3p9/bin/bash' +CC='clang' +export CC +CONFIG_SHELL='/nix/store/3zrx6av2d1141igkcn8477cvbfqpcmcf-bash-5.3p9/bin/bash' +export CONFIG_SHELL +CXX='clang++' +export CXX +DETERMINISTIC_BUILD='1' +export DETERMINISTIC_BUILD +DEVELOPER_DIR='/nix/store/49rnbvkp4nywgr2pqcmii0dr4sbj9zs7-apple-sdk-14.4' +export DEVELOPER_DIR +GOTOOLDIR='/nix/store/vlqqxb8xci50ck2f0dw8rh4mf8mwqyg5-gotools-0.34.0/bin' +export GOTOOLDIR +HOSTTYPE='aarch64' +HOST_PATH='/nix/store/hhdx8wizaynj5203x02wyj7srqyig87r-gitlint-0.19.1/bin:/nix/store/qzgxkh5rnlswnj5ywhag0djwvn83kr3r-python3-3.13.11/bin:/nix/store/d8p3k8wlvbxv4cdpgs0vqv3rr2ck0fqv-treefmt/bin:/nix/store/ik1b3z19bgk8lagf461zq222kxrpdynb-libiconv-109.100.2/bin:/nix/store/chdj76m79c3lbwdn6bflwlqa2n2igx02-coreutils-9.10/bin:/nix/store/laagkkm7lnm746374c4cii932b77pmaw-findutils-4.10.0/bin:/nix/store/0xkxjb418kdssdxx3c8filikjqhzxwcx-diffutils-3.12/bin:/nix/store/xzsihjm86aqgqarrzifrzr4sfchw4225-gnused-4.9/bin:/nix/store/5f96fidrw1fhyv8kxmx1d31m7qymlqdd-gnugrep-3.12/bin:/nix/store/i45ysli6zxj414lyyyhrl19ir3jkfdna-gawk-5.3.2/bin:/nix/store/2q9piicbr4dws7xi4xggb1nqs08k541h-gnutar-1.35/bin:/nix/store/ayfjrsaf6firgmr4h0a2v3g61yl07a6n-gzip-1.14/bin:/nix/store/7bp4xz524b106ay6hxp27k83p50ziv4w-bzip2-1.0.8-bin/bin:/nix/store/l8jkq4dnsknlxz436w6gw5rcm09mvq6p-gnumake-4.4.1/bin:/nix/store/3zrx6av2d1141igkcn8477cvbfqpcmcf-bash-5.3p9/bin:/nix/store/67a37s907bmcms143slallmjnsh2s7w8-patch-2.8/bin:/nix/store/61cj6q1qv2dibpmryaccn4drwc55ysv8-xz-5.8.2-bin/bin:/nix/store/z4nj80nfcllba9n6drf4axylzb24s15k-file-5.45/bin' +export HOST_PATH +IFS=' +' +IN_NIX_SHELL='impure' +export IN_NIX_SHELL +LD='ld' +export LD +LD_DYLD_PATH='/usr/lib/dyld' +export LD_DYLD_PATH +LINENO='80' +MACHTYPE='aarch64-apple-darwin25.2.0' +MACOSX_DEPLOYMENT_TARGET='14.0' +export MACOSX_DEPLOYMENT_TARGET +NIX_APPLE_SDK_VERSION='140400' +export NIX_APPLE_SDK_VERSION +NIX_BINTOOLS='/nix/store/qkqww6ccw4n4px5jzs1fkcg9yh1jnbxl-cctools-binutils-darwin-wrapper-1010.6' +export NIX_BINTOOLS +NIX_BINTOOLS_WRAPPER_TARGET_HOST_arm64_apple_darwin='1' +export NIX_BINTOOLS_WRAPPER_TARGET_HOST_arm64_apple_darwin +NIX_BUILD_CORES='8' +export NIX_BUILD_CORES +NIX_CC='/nix/store/vl936sd1vj8s008akm3gibkgyqk30qhz-clang-wrapper-21.1.8' +export NIX_CC +NIX_CC_WRAPPER_TARGET_HOST_arm64_apple_darwin='1' +export NIX_CC_WRAPPER_TARGET_HOST_arm64_apple_darwin +NIX_CFLAGS_COMPILE=' -frandom-seed=8r540i4dsh -isystem /nix/store/h2y0a3x6hgznqq54rjszbi2x0rdnlgvb-python3-3.13.12/include -fmacro-prefix-map=/nix/store/h2y0a3x6hgznqq54rjszbi2x0rdnlgvb-python3-3.13.12=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.13.12 -isystem /nix/store/g0r7lnyajlqwkkshzwcasa1swnqlr6fs-libcxx-20.1.0+apple-sdk-26.0/include -fmacro-prefix-map=/nix/store/g0r7lnyajlqwkkshzwcasa1swnqlr6fs-libcxx-20.1.0+apple-sdk-26.0=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-libcxx-20.1.0+apple-sdk-26.0 -isystem /nix/store/2adqlqhmwxglc2f0r7fxa5wdlavjn21r-compiler-rt-libc-21.1.8-dev/include -fmacro-prefix-map=/nix/store/2adqlqhmwxglc2f0r7fxa5wdlavjn21r-compiler-rt-libc-21.1.8-dev=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-compiler-rt-libc-21.1.8-dev -isystem /nix/store/qzgxkh5rnlswnj5ywhag0djwvn83kr3r-python3-3.13.11/include -fmacro-prefix-map=/nix/store/qzgxkh5rnlswnj5ywhag0djwvn83kr3r-python3-3.13.11=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.13.11 -isystem /nix/store/gkyk559bh4zzxh0wfvzqi6zj3rdn0d9q-libiconv-109.100.2-dev/include -fmacro-prefix-map=/nix/store/gkyk559bh4zzxh0wfvzqi6zj3rdn0d9q-libiconv-109.100.2-dev=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-libiconv-109.100.2-dev -isystem /nix/store/y02fcdl0bx02pkmbsbxks8nmy7yxvanf-libresolv-91-dev/include -fmacro-prefix-map=/nix/store/y02fcdl0bx02pkmbsbxks8nmy7yxvanf-libresolv-91-dev=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-libresolv-91-dev -isystem /nix/store/w1q5dsq93j18clmn9pzqgl88n7v5j2v9-libsbuf-14.1.0-dev/include -fmacro-prefix-map=/nix/store/w1q5dsq93j18clmn9pzqgl88n7v5j2v9-libsbuf-14.1.0-dev=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-libsbuf-14.1.0-dev -isystem /nix/store/h2y0a3x6hgznqq54rjszbi2x0rdnlgvb-python3-3.13.12/include -fmacro-prefix-map=/nix/store/h2y0a3x6hgznqq54rjszbi2x0rdnlgvb-python3-3.13.12=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.13.12 -isystem /nix/store/g0r7lnyajlqwkkshzwcasa1swnqlr6fs-libcxx-20.1.0+apple-sdk-26.0/include -fmacro-prefix-map=/nix/store/g0r7lnyajlqwkkshzwcasa1swnqlr6fs-libcxx-20.1.0+apple-sdk-26.0=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-libcxx-20.1.0+apple-sdk-26.0 -isystem /nix/store/2adqlqhmwxglc2f0r7fxa5wdlavjn21r-compiler-rt-libc-21.1.8-dev/include -fmacro-prefix-map=/nix/store/2adqlqhmwxglc2f0r7fxa5wdlavjn21r-compiler-rt-libc-21.1.8-dev=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-compiler-rt-libc-21.1.8-dev -isystem /nix/store/qzgxkh5rnlswnj5ywhag0djwvn83kr3r-python3-3.13.11/include -fmacro-prefix-map=/nix/store/qzgxkh5rnlswnj5ywhag0djwvn83kr3r-python3-3.13.11=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.13.11 -isystem /nix/store/gkyk559bh4zzxh0wfvzqi6zj3rdn0d9q-libiconv-109.100.2-dev/include -fmacro-prefix-map=/nix/store/gkyk559bh4zzxh0wfvzqi6zj3rdn0d9q-libiconv-109.100.2-dev=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-libiconv-109.100.2-dev -isystem /nix/store/y02fcdl0bx02pkmbsbxks8nmy7yxvanf-libresolv-91-dev/include -fmacro-prefix-map=/nix/store/y02fcdl0bx02pkmbsbxks8nmy7yxvanf-libresolv-91-dev=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-libresolv-91-dev -isystem /nix/store/w1q5dsq93j18clmn9pzqgl88n7v5j2v9-libsbuf-14.1.0-dev/include -fmacro-prefix-map=/nix/store/w1q5dsq93j18clmn9pzqgl88n7v5j2v9-libsbuf-14.1.0-dev=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-libsbuf-14.1.0-dev' +export NIX_CFLAGS_COMPILE +NIX_DONT_SET_RPATH='1' +export NIX_DONT_SET_RPATH +NIX_DONT_SET_RPATH_FOR_BUILD='1' +export NIX_DONT_SET_RPATH_FOR_BUILD +NIX_ENFORCE_NO_NATIVE='1' +export NIX_ENFORCE_NO_NATIVE +NIX_HARDENING_ENABLE='bindnow format fortify fortify3 libcxxhardeningextensive libcxxhardeningfast pic relro stackclashprotection stackprotector strictoverflow zerocallusedregs' +export NIX_HARDENING_ENABLE +NIX_IGNORE_LD_THROUGH_GCC='1' +export NIX_IGNORE_LD_THROUGH_GCC +NIX_LDFLAGS=' -L/nix/store/h2y0a3x6hgznqq54rjszbi2x0rdnlgvb-python3-3.13.12/lib -L/nix/store/g0r7lnyajlqwkkshzwcasa1swnqlr6fs-libcxx-20.1.0+apple-sdk-26.0/lib -L/nix/store/16j7rnr03s8ssn0q9nwf3ncj7aj2pqd3-compiler-rt-libc-21.1.8/lib -L/nix/store/qzgxkh5rnlswnj5ywhag0djwvn83kr3r-python3-3.13.11/lib -L/nix/store/ik1b3z19bgk8lagf461zq222kxrpdynb-libiconv-109.100.2/lib -L/nix/store/r0vyrwqczqhfa64ybmz1ag57ssvbxakh-libresolv-91/lib -L/nix/store/q0l5wfnkkhynp14mpl1pk33xhzsaif38-libsbuf-14.1.0/lib -L/nix/store/0zwkfp018d8qpx2lyppvn07rbgvblps4-libutil-72/lib -L/nix/store/h2y0a3x6hgznqq54rjszbi2x0rdnlgvb-python3-3.13.12/lib -L/nix/store/g0r7lnyajlqwkkshzwcasa1swnqlr6fs-libcxx-20.1.0+apple-sdk-26.0/lib -L/nix/store/16j7rnr03s8ssn0q9nwf3ncj7aj2pqd3-compiler-rt-libc-21.1.8/lib -L/nix/store/qzgxkh5rnlswnj5ywhag0djwvn83kr3r-python3-3.13.11/lib -L/nix/store/ik1b3z19bgk8lagf461zq222kxrpdynb-libiconv-109.100.2/lib -L/nix/store/r0vyrwqczqhfa64ybmz1ag57ssvbxakh-libresolv-91/lib -L/nix/store/q0l5wfnkkhynp14mpl1pk33xhzsaif38-libsbuf-14.1.0/lib -L/nix/store/0zwkfp018d8qpx2lyppvn07rbgvblps4-libutil-72/lib' +export NIX_LDFLAGS +NIX_NO_SELF_RPATH='1' +export NIX_NO_SELF_RPATH +NIX_STORE='/nix/store' +export NIX_STORE +NM='nm' +export NM +OBJCOPY='objcopy' +export OBJCOPY +OBJDUMP='objdump' +export OBJDUMP +OLDPWD='' +export OLDPWD +OPTERR='1' +OSTYPE='darwin25.2.0' +PATH='/nix/store/igl44md7ljf5c3hx6s6ys6325fkpldny-nixfmt-1.2.0/bin:/nix/store/4ygyyjm4ad5q9382wiik7nn59b60rk63-gitlint-0.19.1/bin:/nix/store/h2y0a3x6hgznqq54rjszbi2x0rdnlgvb-python3-3.13.12/bin:/nix/store/r0xwfwva8p84gqlv5jp8icl26mk0zw76-gitleaks-8.30.0/bin:/nix/store/sc8kb91395fw69qcmij89m3bxdmj8gwb-shfmt-3.12.0/bin:/nix/store/7x4dmbrjq24hgpzs1g6jxi3gm207ypkd-bun-1.3.10/bin:/nix/store/5ajixjk279m40yf6x96xxlnvw1wg6hq3-go-1.26.0/bin:/nix/store/5498nh3pn799k53gwqlpmzwhvapj7qn6-gopls-0.21.1/bin:/nix/store/vlqqxb8xci50ck2f0dw8rh4mf8mwqyg5-gotools-0.34.0/bin:/nix/store/62jd6i5pz1rc4ilnbgsk6ialpbm3xm8j-bazel-buildtools-8.5.1/bin:/nix/store/1xwbwcvzk5c0yzwmix9dimcmgdf61gr4-bazel-watcher-0.28.0/bin:/nix/store/59rqcmzdr53ay0pfgqc3mpa9l9lr1wx6-oxfmt-0.27.0/bin:/nix/store/xdyrcv7zr12n61nz4fzjf5knix0nj0xx-bazel/bin:/nix/store/g3r24xyhs1k9yba5166521fvvpr5rw77-wails3-3.0.0-alpha.74/bin:/nix/store/vl936sd1vj8s008akm3gibkgyqk30qhz-clang-wrapper-21.1.8/bin:/nix/store/rr64nnycczvx7s1b110qmvqrlfcb6lsm-clang-21.1.8/bin:/nix/store/chdj76m79c3lbwdn6bflwlqa2n2igx02-coreutils-9.10/bin:/nix/store/qkqww6ccw4n4px5jzs1fkcg9yh1jnbxl-cctools-binutils-darwin-wrapper-1010.6/bin:/nix/store/020z53qny5xbgxi1r5j3l27r135zmrjy-cctools-binutils-darwin-1010.6/bin:/nix/store/4wy2b9dipk7h7c6i444ipys9kq00f4qc-xcbuild-0.1.1-unstable-2019-11-20-xcrun/bin:/nix/store/hhdx8wizaynj5203x02wyj7srqyig87r-gitlint-0.19.1/bin:/nix/store/qzgxkh5rnlswnj5ywhag0djwvn83kr3r-python3-3.13.11/bin:/nix/store/d8p3k8wlvbxv4cdpgs0vqv3rr2ck0fqv-treefmt/bin:/nix/store/ik1b3z19bgk8lagf461zq222kxrpdynb-libiconv-109.100.2/bin:/nix/store/chdj76m79c3lbwdn6bflwlqa2n2igx02-coreutils-9.10/bin:/nix/store/laagkkm7lnm746374c4cii932b77pmaw-findutils-4.10.0/bin:/nix/store/0xkxjb418kdssdxx3c8filikjqhzxwcx-diffutils-3.12/bin:/nix/store/xzsihjm86aqgqarrzifrzr4sfchw4225-gnused-4.9/bin:/nix/store/5f96fidrw1fhyv8kxmx1d31m7qymlqdd-gnugrep-3.12/bin:/nix/store/i45ysli6zxj414lyyyhrl19ir3jkfdna-gawk-5.3.2/bin:/nix/store/2q9piicbr4dws7xi4xggb1nqs08k541h-gnutar-1.35/bin:/nix/store/ayfjrsaf6firgmr4h0a2v3g61yl07a6n-gzip-1.14/bin:/nix/store/7bp4xz524b106ay6hxp27k83p50ziv4w-bzip2-1.0.8-bin/bin:/nix/store/l8jkq4dnsknlxz436w6gw5rcm09mvq6p-gnumake-4.4.1/bin:/nix/store/3zrx6av2d1141igkcn8477cvbfqpcmcf-bash-5.3p9/bin:/nix/store/67a37s907bmcms143slallmjnsh2s7w8-patch-2.8/bin:/nix/store/61cj6q1qv2dibpmryaccn4drwc55ysv8-xz-5.8.2-bin/bin:/nix/store/z4nj80nfcllba9n6drf4axylzb24s15k-file-5.45/bin' +export PATH +PATH_LOCALE='/nix/store/njmig5gp6llav5ds1yhvi7r0l38ihds3-locale-118/share/locale' +export PATH_LOCALE +PS4='+ ' +PYTHONHASHSEED='0' +export PYTHONHASHSEED +PYTHONNOUSERSITE='1' +export PYTHONNOUSERSITE +PYTHONPATH='/nix/store/4ygyyjm4ad5q9382wiik7nn59b60rk63-gitlint-0.19.1/lib/python3.13/site-packages:/nix/store/3xza1grhkwh3mnwd0j29a9hsbbgxhdak-python3.13-arrow-1.4.0/lib/python3.13/site-packages:/nix/store/qflvkqlgrxij13pljw5bfjjy8293423y-python3.13-python-dateutil-2.9.0.post0/lib/python3.13/site-packages:/nix/store/3fklla4b6nagqd4f0khfnbzc9fq0ff5a-python3.13-six-1.17.0/lib/python3.13/site-packages:/nix/store/h2y0a3x6hgznqq54rjszbi2x0rdnlgvb-python3-3.13.12/lib/python3.13/site-packages:/nix/store/8ilq4kw137gpq10qibiv6rz2xkwq56d8-python3.13-types-python-dateutil-2.9.0.20251115/lib/python3.13/site-packages:/nix/store/pqb998q9642lgg0fb1r8msr0hkzdyg14-python3.13-tzdata-2025.3/lib/python3.13/site-packages:/nix/store/h7wda1dhw18x43b6g4px3kk18m542qaq-python3.13-click-8.3.1/lib/python3.13/site-packages:/nix/store/n8qcvx32nlj1g10py7fg1js051vp7pfd-python3.13-sh-2.2.2/lib/python3.13/site-packages:/nix/store/hhdx8wizaynj5203x02wyj7srqyig87r-gitlint-0.19.1/lib/python3.13/site-packages:/nix/store/yvgibssnyvi4jfcqw3zs8hx07cih80ay-python3.13-arrow-1.3.0/lib/python3.13/site-packages:/nix/store/bdbzfjw1gvyk6lziqxkv7kiwddqk3ncg-python3.13-python-dateutil-2.9.0.post0/lib/python3.13/site-packages:/nix/store/kgkc9bbp2s5k30ryd8pgv7gcbh30cm3f-python3.13-six-1.17.0/lib/python3.13/site-packages:/nix/store/qzgxkh5rnlswnj5ywhag0djwvn83kr3r-python3-3.13.11/lib/python3.13/site-packages:/nix/store/kbhcff5i9v3svjahb5wwjcyc0im5rr98-python3.13-types-python-dateutil-2.9.0.20250708/lib/python3.13/site-packages:/nix/store/nlvajmiq1nwji4bzcz6r5q7y571khj05-python3.13-click-8.3.1/lib/python3.13/site-packages:/nix/store/p030824m0k4zwkicdm1cgz9khvfapbr1-python3.13-sh-2.2.2/lib/python3.13/site-packages' +export PYTHONPATH +RANLIB='ranlib' +export RANLIB +SDKROOT='/nix/store/49rnbvkp4nywgr2pqcmii0dr4sbj9zs7-apple-sdk-14.4/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk' +export SDKROOT +SHELL='/nix/store/3zrx6av2d1141igkcn8477cvbfqpcmcf-bash-5.3p9/bin/bash' +export SHELL +SIZE='size' +export SIZE +SOURCE_DATE_EPOCH='315532800' +export SOURCE_DATE_EPOCH +STRINGS='strings' +export STRINGS +STRIP='strip' +export STRIP +XDG_DATA_DIRS='/nix/store/igl44md7ljf5c3hx6s6ys6325fkpldny-nixfmt-1.2.0/share:/nix/store/h2y0a3x6hgznqq54rjszbi2x0rdnlgvb-python3-3.13.12/share:/nix/store/r0xwfwva8p84gqlv5jp8icl26mk0zw76-gitleaks-8.30.0/share:/nix/store/sc8kb91395fw69qcmij89m3bxdmj8gwb-shfmt-3.12.0/share:/nix/store/7x4dmbrjq24hgpzs1g6jxi3gm207ypkd-bun-1.3.10/share:/nix/store/5ajixjk279m40yf6x96xxlnvw1wg6hq3-go-1.26.0/share' +export XDG_DATA_DIRS +ZERO_AR_DATE='1' +export ZERO_AR_DATE +__darwinAllowLocalNetworking='' +export __darwinAllowLocalNetworking +__impureHostDeps='/bin/sh /usr/lib/libSystem.B.dylib /usr/lib/system/libunc.dylib /dev/zero /dev/random /dev/urandom /bin/sh' +export __impureHostDeps +__propagatedImpureHostDeps='' +export __propagatedImpureHostDeps +__propagatedSandboxProfile='' +export __propagatedSandboxProfile +__sandboxProfile='' +export __sandboxProfile +__structuredAttrs='' +export __structuredAttrs +_substituteStream_has_warned_replace_deprecation='false' +buildInputs='/nix/store/hhdx8wizaynj5203x02wyj7srqyig87r-gitlint-0.19.1 /nix/store/d8p3k8wlvbxv4cdpgs0vqv3rr2ck0fqv-treefmt' +export buildInputs +buildPhase='{ echo "------------------------------------------------------------"; + echo " WARNING: the existence of this path is not guaranteed."; + echo " It is an internal implementation detail for pkgs.mkShell."; + echo "------------------------------------------------------------"; + echo; + # Record all build inputs as runtime dependencies + export; +} >> "$out" +' +export buildPhase +builder='/nix/store/3zrx6av2d1141igkcn8477cvbfqpcmcf-bash-5.3p9/bin/bash' +export builder +cmakeFlags='' +export cmakeFlags +configureFlags='' +export configureFlags +defaultBuildInputs='/nix/store/49rnbvkp4nywgr2pqcmii0dr4sbj9zs7-apple-sdk-14.4' +defaultNativeBuildInputs='/nix/store/23c6sxdfaa5r38ihc7y575vv7j4qs4sk-update-autotools-gnu-config-scripts-hook /nix/store/0y5xmdb7qfvimjwbq7ibg1xdgkgjwqng-no-broken-symlinks.sh /nix/store/cv1d7p48379km6a85h4zp6kr86brh32q-audit-tmpdir.sh /nix/store/85clx3b0xkdf58jn161iy80y5223ilbi-compress-man-pages.sh /nix/store/p3l1a5y7nllfyrjn2krlwgcc3z0cd3fq-make-symlinks-relative.sh /nix/store/5yzw0vhkyszf2d179m0qfkgxmp5wjjx4-move-docs.sh /nix/store/fyaryjvghbkpfnsyw97hb3lyb37s1pd6-move-lib64.sh /nix/store/kd4xwxjpjxi71jkm6ka0np72if9rm3y0-move-sbin.sh /nix/store/pag6l61paj1dc9sv15l7bm5c17xn5kyk-move-systemd-user-units.sh /nix/store/cmzya9irvxzlkh7lfy6i82gbp0saxqj3-multiple-outputs.sh /nix/store/x8c40nfigps493a07sdr2pm5s9j1cdc0-patch-shebangs.sh /nix/store/cickvswrvann041nqxb0rxilc46svw1n-prune-libtool-files.sh /nix/store/xyff06pkhki3qy1ls77w10s0v79c9il0-reproducible-builds.sh /nix/store/z7k98578dfzi6l3hsvbivzm7hfqlk0zc-set-source-date-epoch-to-latest.sh /nix/store/pilsssjjdxvdphlg2h19p0bfx5q0jzkn-strip.sh /nix/store/vl936sd1vj8s008akm3gibkgyqk30qhz-clang-wrapper-21.1.8' +depsBuildBuild='' +export depsBuildBuild +depsBuildBuildPropagated='' +export depsBuildBuildPropagated +depsBuildTarget='' +export depsBuildTarget +depsBuildTargetPropagated='' +export depsBuildTargetPropagated +depsHostHost='' +export depsHostHost +depsHostHostPropagated='' +export depsHostHostPropagated +depsTargetTarget='' +export depsTargetTarget +depsTargetTargetPropagated='' +export depsTargetTargetPropagated +doCheck='' +export doCheck +doInstallCheck='' +export doInstallCheck +dontAddDisableDepTrack='1' +export dontAddDisableDepTrack +declare -a envBuildBuildHooks=('addPythonPath' ) +declare -a envBuildHostHooks=('addPythonPath' ) +declare -a envBuildTargetHooks=('addPythonPath' ) +declare -a envHostHostHooks=('ccWrapper_addCVars' 'bintoolsWrapper_addLDVars' 'addPythonPath' ) +declare -a envHostTargetHooks=('ccWrapper_addCVars' 'bintoolsWrapper_addLDVars' 'addPythonPath' ) +declare -a envTargetTargetHooks=() +declare -a fixupOutputHooks=('if [[ -z "${noAuditTmpdir-}" && -e "$prefix" ]]; then auditTmpdir "$prefix"; fi' 'if [ -z "${dontGzipMan-}" ]; then compressManPages "$prefix"; fi' '_moveLib64' '_moveSbin' '_moveSystemdUserUnits' 'patchShebangsAuto' '_pruneLibtoolFiles' '_doStrip' ) +initialPath='/nix/store/chdj76m79c3lbwdn6bflwlqa2n2igx02-coreutils-9.10 /nix/store/laagkkm7lnm746374c4cii932b77pmaw-findutils-4.10.0 /nix/store/0xkxjb418kdssdxx3c8filikjqhzxwcx-diffutils-3.12 /nix/store/xzsihjm86aqgqarrzifrzr4sfchw4225-gnused-4.9 /nix/store/5f96fidrw1fhyv8kxmx1d31m7qymlqdd-gnugrep-3.12 /nix/store/i45ysli6zxj414lyyyhrl19ir3jkfdna-gawk-5.3.2 /nix/store/2q9piicbr4dws7xi4xggb1nqs08k541h-gnutar-1.35 /nix/store/ayfjrsaf6firgmr4h0a2v3g61yl07a6n-gzip-1.14 /nix/store/7bp4xz524b106ay6hxp27k83p50ziv4w-bzip2-1.0.8-bin /nix/store/l8jkq4dnsknlxz436w6gw5rcm09mvq6p-gnumake-4.4.1 /nix/store/3zrx6av2d1141igkcn8477cvbfqpcmcf-bash-5.3p9 /nix/store/67a37s907bmcms143slallmjnsh2s7w8-patch-2.8 /nix/store/61cj6q1qv2dibpmryaccn4drwc55ysv8-xz-5.8.2-bin /nix/store/z4nj80nfcllba9n6drf4axylzb24s15k-file-5.45' +mesonFlags='' +export mesonFlags +name='nix-shell-env' +export name +nativeBuildInputs='/nix/store/igl44md7ljf5c3hx6s6ys6325fkpldny-nixfmt-1.2.0 /nix/store/4ygyyjm4ad5q9382wiik7nn59b60rk63-gitlint-0.19.1 /nix/store/r0xwfwva8p84gqlv5jp8icl26mk0zw76-gitleaks-8.30.0 /nix/store/sc8kb91395fw69qcmij89m3bxdmj8gwb-shfmt-3.12.0 /nix/store/7x4dmbrjq24hgpzs1g6jxi3gm207ypkd-bun-1.3.10 /nix/store/5ajixjk279m40yf6x96xxlnvw1wg6hq3-go-1.26.0 /nix/store/5498nh3pn799k53gwqlpmzwhvapj7qn6-gopls-0.21.1 /nix/store/vlqqxb8xci50ck2f0dw8rh4mf8mwqyg5-gotools-0.34.0 /nix/store/62jd6i5pz1rc4ilnbgsk6ialpbm3xm8j-bazel-buildtools-8.5.1 /nix/store/1xwbwcvzk5c0yzwmix9dimcmgdf61gr4-bazel-watcher-0.28.0 /nix/store/59rqcmzdr53ay0pfgqc3mpa9l9lr1wx6-oxfmt-0.27.0 /nix/store/xdyrcv7zr12n61nz4fzjf5knix0nj0xx-bazel /nix/store/g3r24xyhs1k9yba5166521fvvpr5rw77-wails3-3.0.0-alpha.74' +export nativeBuildInputs +out='/Users/eric/Projects/wails_tools/outputs/out' +export out +outputBin='out' +outputDev='out' +outputDevdoc='REMOVE' +outputDevman='out' +outputDoc='out' +outputInclude='out' +outputInfo='out' +outputLib='out' +outputMan='out' +outputs='out' +export outputs +patches='' +export patches +phases='buildPhase' +export phases +pkg='/nix/store/49rnbvkp4nywgr2pqcmii0dr4sbj9zs7-apple-sdk-14.4' +declare -a pkgsBuildBuild=() +declare -a pkgsBuildHost=('/nix/store/igl44md7ljf5c3hx6s6ys6325fkpldny-nixfmt-1.2.0' '/nix/store/4ygyyjm4ad5q9382wiik7nn59b60rk63-gitlint-0.19.1' '/nix/store/3xza1grhkwh3mnwd0j29a9hsbbgxhdak-python3.13-arrow-1.4.0' '/nix/store/qflvkqlgrxij13pljw5bfjjy8293423y-python3.13-python-dateutil-2.9.0.post0' '/nix/store/3fklla4b6nagqd4f0khfnbzc9fq0ff5a-python3.13-six-1.17.0' '/nix/store/h2y0a3x6hgznqq54rjszbi2x0rdnlgvb-python3-3.13.12' '/nix/store/8ilq4kw137gpq10qibiv6rz2xkwq56d8-python3.13-types-python-dateutil-2.9.0.20251115' '/nix/store/pqb998q9642lgg0fb1r8msr0hkzdyg14-python3.13-tzdata-2025.3' '/nix/store/h7wda1dhw18x43b6g4px3kk18m542qaq-python3.13-click-8.3.1' '/nix/store/n8qcvx32nlj1g10py7fg1js051vp7pfd-python3.13-sh-2.2.2' '/nix/store/r0xwfwva8p84gqlv5jp8icl26mk0zw76-gitleaks-8.30.0' '/nix/store/sc8kb91395fw69qcmij89m3bxdmj8gwb-shfmt-3.12.0' '/nix/store/7x4dmbrjq24hgpzs1g6jxi3gm207ypkd-bun-1.3.10' '/nix/store/5ajixjk279m40yf6x96xxlnvw1wg6hq3-go-1.26.0' '/nix/store/5498nh3pn799k53gwqlpmzwhvapj7qn6-gopls-0.21.1' '/nix/store/vlqqxb8xci50ck2f0dw8rh4mf8mwqyg5-gotools-0.34.0' '/nix/store/62jd6i5pz1rc4ilnbgsk6ialpbm3xm8j-bazel-buildtools-8.5.1' '/nix/store/1xwbwcvzk5c0yzwmix9dimcmgdf61gr4-bazel-watcher-0.28.0' '/nix/store/59rqcmzdr53ay0pfgqc3mpa9l9lr1wx6-oxfmt-0.27.0' '/nix/store/xdyrcv7zr12n61nz4fzjf5knix0nj0xx-bazel' '/nix/store/g3r24xyhs1k9yba5166521fvvpr5rw77-wails3-3.0.0-alpha.74' '/nix/store/23c6sxdfaa5r38ihc7y575vv7j4qs4sk-update-autotools-gnu-config-scripts-hook' '/nix/store/0y5xmdb7qfvimjwbq7ibg1xdgkgjwqng-no-broken-symlinks.sh' '/nix/store/cv1d7p48379km6a85h4zp6kr86brh32q-audit-tmpdir.sh' '/nix/store/85clx3b0xkdf58jn161iy80y5223ilbi-compress-man-pages.sh' '/nix/store/p3l1a5y7nllfyrjn2krlwgcc3z0cd3fq-make-symlinks-relative.sh' '/nix/store/5yzw0vhkyszf2d179m0qfkgxmp5wjjx4-move-docs.sh' '/nix/store/fyaryjvghbkpfnsyw97hb3lyb37s1pd6-move-lib64.sh' '/nix/store/kd4xwxjpjxi71jkm6ka0np72if9rm3y0-move-sbin.sh' '/nix/store/pag6l61paj1dc9sv15l7bm5c17xn5kyk-move-systemd-user-units.sh' '/nix/store/cmzya9irvxzlkh7lfy6i82gbp0saxqj3-multiple-outputs.sh' '/nix/store/x8c40nfigps493a07sdr2pm5s9j1cdc0-patch-shebangs.sh' '/nix/store/cickvswrvann041nqxb0rxilc46svw1n-prune-libtool-files.sh' '/nix/store/xyff06pkhki3qy1ls77w10s0v79c9il0-reproducible-builds.sh' '/nix/store/z7k98578dfzi6l3hsvbivzm7hfqlk0zc-set-source-date-epoch-to-latest.sh' '/nix/store/pilsssjjdxvdphlg2h19p0bfx5q0jzkn-strip.sh' '/nix/store/vl936sd1vj8s008akm3gibkgyqk30qhz-clang-wrapper-21.1.8' '/nix/store/qkqww6ccw4n4px5jzs1fkcg9yh1jnbxl-cctools-binutils-darwin-wrapper-1010.6' '/nix/store/4wy2b9dipk7h7c6i444ipys9kq00f4qc-xcbuild-0.1.1-unstable-2019-11-20-xcrun' ) +declare -a pkgsBuildTarget=() +declare -a pkgsHostHost=('/nix/store/g0r7lnyajlqwkkshzwcasa1swnqlr6fs-libcxx-20.1.0+apple-sdk-26.0' '/nix/store/2adqlqhmwxglc2f0r7fxa5wdlavjn21r-compiler-rt-libc-21.1.8-dev' '/nix/store/16j7rnr03s8ssn0q9nwf3ncj7aj2pqd3-compiler-rt-libc-21.1.8' ) +declare -a pkgsHostTarget=('/nix/store/hhdx8wizaynj5203x02wyj7srqyig87r-gitlint-0.19.1' '/nix/store/yvgibssnyvi4jfcqw3zs8hx07cih80ay-python3.13-arrow-1.3.0' '/nix/store/bdbzfjw1gvyk6lziqxkv7kiwddqk3ncg-python3.13-python-dateutil-2.9.0.post0' '/nix/store/kgkc9bbp2s5k30ryd8pgv7gcbh30cm3f-python3.13-six-1.17.0' '/nix/store/qzgxkh5rnlswnj5ywhag0djwvn83kr3r-python3-3.13.11' '/nix/store/kbhcff5i9v3svjahb5wwjcyc0im5rr98-python3.13-types-python-dateutil-2.9.0.20250708' '/nix/store/nlvajmiq1nwji4bzcz6r5q7y571khj05-python3.13-click-8.3.1' '/nix/store/p030824m0k4zwkicdm1cgz9khvfapbr1-python3.13-sh-2.2.2' '/nix/store/d8p3k8wlvbxv4cdpgs0vqv3rr2ck0fqv-treefmt' '/nix/store/49rnbvkp4nywgr2pqcmii0dr4sbj9zs7-apple-sdk-14.4' '/nix/store/gkyk559bh4zzxh0wfvzqi6zj3rdn0d9q-libiconv-109.100.2-dev' '/nix/store/ik1b3z19bgk8lagf461zq222kxrpdynb-libiconv-109.100.2' '/nix/store/y02fcdl0bx02pkmbsbxks8nmy7yxvanf-libresolv-91-dev' '/nix/store/r0vyrwqczqhfa64ybmz1ag57ssvbxakh-libresolv-91' '/nix/store/w1q5dsq93j18clmn9pzqgl88n7v5j2v9-libsbuf-14.1.0-dev' '/nix/store/q0l5wfnkkhynp14mpl1pk33xhzsaif38-libsbuf-14.1.0' '/nix/store/0zwkfp018d8qpx2lyppvn07rbgvblps4-libutil-72' ) +declare -a pkgsTargetTarget=() +declare -a postFixupHooks=('noBrokenSymlinksInAllOutputs' '_makeSymlinksRelative' '_multioutPropagateDev' ) +declare -a postUnpackHooks=('_updateSourceDateEpochFromSourceRoot' ) +declare -a preConfigureHooks=('_multioutConfig' ) +preConfigurePhases=' updateAutotoolsGnuConfigScriptsPhase' +declare -a preFixupHooks=('_moveToShare' '_multioutDocs' '_multioutDevs' ) +preferLocalBuild='1' +export preferLocalBuild +prefix='/Users/eric/Projects/wails_tools/outputs/out' +declare -a propagatedBuildDepFiles=('propagated-build-build-deps' 'propagated-native-build-inputs' 'propagated-build-target-deps' ) +propagatedBuildInputs='' +export propagatedBuildInputs +declare -a propagatedHostDepFiles=('propagated-host-host-deps' 'propagated-build-inputs' ) +propagatedNativeBuildInputs='' +export propagatedNativeBuildInputs +declare -a propagatedTargetDepFiles=('propagated-target-target-deps' ) +shell='/nix/store/3zrx6av2d1141igkcn8477cvbfqpcmcf-bash-5.3p9/bin/bash' +export shell +shellHook='if true; then + if ! /nix/store/dbd3ngiax26c6grkriygc14vdwfqwlvv-git-minimal-2.52.0/bin/git rev-parse --git-dir &> /dev/null; then + echo 1>&2 "WARNING: git-hooks.nix: .git not found; skipping installation." + else + GIT_WC=`/nix/store/dbd3ngiax26c6grkriygc14vdwfqwlvv-git-minimal-2.52.0/bin/git rev-parse --show-toplevel` + + # These update procedures compare before they write, to avoid + # filesystem churn. This improves performance with watch tools like lorri + # and prevents installation loops by lorri. + + if ! readlink "${GIT_WC}/.pre-commit-config.yaml" >/dev/null \ + || [[ $(readlink "${GIT_WC}/.pre-commit-config.yaml") != /nix/store/0qfkflnjv85f31nikijv5a5icjn5xa5m-pre-commit-config.json ]]; then + echo 1>&2 "git-hooks.nix: updating $PWD repo" + [ -L "${GIT_WC}/.pre-commit-config.yaml" ] && unlink "${GIT_WC}/.pre-commit-config.yaml" + + if [ -e "${GIT_WC}/.pre-commit-config.yaml" ]; then + echo 1>&2 "git-hooks.nix: WARNING: Refusing to install because of an existing config at .pre-commit-config.yaml" + echo 1>&2 "" + echo 1>&2 " To migrate the existing config to a Nix configuration:" + echo 1>&2 " 1. Translate the contents of .pre-commit-config.yaml into a Nix configuration." + echo 1>&2 " See https://github.com/cachix/git-hooks.nix#getting-started" + echo 1>&2 " 2. Remove .pre-commit-config.yaml" + echo 1>&2 " 3. Add .pre-commit-config.yaml to .gitignore" + else + if true; then + nix-store --add-root "${GIT_WC}/.pre-commit-config.yaml" --indirect --realise /nix/store/0qfkflnjv85f31nikijv5a5icjn5xa5m-pre-commit-config.json + else + ln -fs /nix/store/0qfkflnjv85f31nikijv5a5icjn5xa5m-pre-commit-config.json "${GIT_WC}/.pre-commit-config.yaml" + fi + # Remove any previously installed hooks (since pre-commit itself has no convergent design) + hooks="commit-msg post-checkout post-commit post-merge post-rewrite pre-commit pre-merge-commit pre-push pre-rebase prepare-commit-msg" + for hook in $hooks; do + /nix/store/j5pi70fd5x4qjcvdvk2bac03zl9wwims-pre-commit-4.5.1/bin/pre-commit uninstall -t $hook + done + /nix/store/dbd3ngiax26c6grkriygc14vdwfqwlvv-git-minimal-2.52.0/bin/git config --local core.hooksPath "" + # Add hooks for configured stages (only) ... + if [ ! -z "pre-commit commit-msg pre-push" ]; then + for stage in pre-commit commit-msg pre-push; do + case $stage in + manual) + ;; + # if you amend these switches please also review $hooks above + commit | merge-commit | push) + stage="pre-"$stage + /nix/store/j5pi70fd5x4qjcvdvk2bac03zl9wwims-pre-commit-4.5.1/bin/pre-commit install -c .pre-commit-config.yaml -t $stage + ;; + commit-msg|post-checkout|post-commit|post-merge|post-rewrite|pre-commit|pre-merge-commit|pre-push|pre-rebase|prepare-commit-msg|manual) + /nix/store/j5pi70fd5x4qjcvdvk2bac03zl9wwims-pre-commit-4.5.1/bin/pre-commit install -c .pre-commit-config.yaml -t $stage + ;; + *) + echo 1>&2 "ERROR: git-hooks.nix: either $stage is not a valid stage or git-hooks.nix doesn'\''t yet support it." + exit 1 + ;; + esac + done + # ... or default '\''pre-commit'\'' hook + else + /nix/store/j5pi70fd5x4qjcvdvk2bac03zl9wwims-pre-commit-4.5.1/bin/pre-commit install -c .pre-commit-config.yaml + fi + + # Fetch the absolute path to the git common directory. This will normally point to $GIT_WC/.git. + common_dir=$(/nix/store/dbd3ngiax26c6grkriygc14vdwfqwlvv-git-minimal-2.52.0/bin/git rev-parse --path-format=absolute --git-common-dir) + + # Convert the absolute path to a path relative to the toplevel working directory. + common_dir=${common_dir#$GIT_WC/} + + /nix/store/dbd3ngiax26c6grkriygc14vdwfqwlvv-git-minimal-2.52.0/bin/git config --local core.hooksPath "$common_dir/hooks" + fi + fi +fi +fi + +export PATH=/nix/store/j5pi70fd5x4qjcvdvk2bac03zl9wwims-pre-commit-4.5.1/bin:$PATH + + +if [ -t 1 ]; then + command -v tput >/dev/null 2>&1 && tput clear || printf '\''\033c'\'' +fi + +GREEN=$'\''\033[1;32m'\'' +CYAN=$'\''\033[1;36m'\'' +YELLOW=$'\''\033[1;33m'\'' +BLUE=$'\''\033[1;34m'\'' +RED=$'\''\033[1;31m'\'' +MAGENTA=$'\''\033[1;35m'\'' +WHITE=$'\''\033[1;37m'\'' +GRAY=$'\''\033[0;90m'\'' +BOLD=$'\''\033[1m'\'' +UNDERLINE=$'\''\033[4m'\'' +RESET=$'\''\033[0m'\'' + +repo_lib_probe_tool() { + local name="$1" + local color_name="$2" + local required="$3" + local line_no="$4" + local group_no="$5" + local regex="$6" + local executable="$7" + shift 7 + + local color="${!color_name:-$YELLOW}" + local output="" + local selected="" + local version="" + + if ! output="$("$executable" "$@" 2>&1)"; then + printf " $CYAN %-6s$RESET $RED%s$RESET\n" "${name}:" "probe failed" + printf "%s\n" "$output" >&2 + if [ "$required" = "1" ]; then + exit 1 + fi + return 0 + fi + + selected="$(printf '\''%s\n'\'' "$output" | sed -n "${line_no}p")" + selected="$(printf '\''%s'\'' "$selected" | sed -E '\''s/^[[:space:]]+//; s/[[:space:]]+$//'\'')" + + if [ -n "$regex" ]; then + if [[ "$selected" =~ $regex ]]; then + version="${BASH_REMATCH[$group_no]}" + else + printf " $CYAN %-6s$RESET $RED%s$RESET\n" "${name}:" "version parse failed" + printf "%s\n" "$output" >&2 + if [ "$required" = "1" ]; then + exit 1 + fi + return 0 + fi + else + version="$selected" + fi + + if [ -z "$version" ]; then + printf " $CYAN %-6s$RESET $RED%s$RESET\n" "${name}:" "empty version" + printf "%s\n" "$output" >&2 + if [ "$required" = "1" ]; then + exit 1 + fi + return 0 + fi + + printf " $CYAN %-6s$RESET %s%s$RESET\n" "${name}:" "$color" "$version" +} + +repo_lib_probe_legacy_tool() { + local name="$1" + local color_name="$2" + local required="$3" + local command_name="$4" + local version_command="$5" + + local color="${!color_name:-$YELLOW}" + local output="" + local version="" + + if ! command -v "$command_name" >/dev/null 2>&1; then + if [ "$required" = "1" ]; then + printf " $CYAN %-6s$RESET $RED%s$RESET\n" "${name}:" "missing command" + exit 1 + fi + return 0 + fi + + if ! output="$(sh -c "$command_name $version_command" 2>&1)"; then + printf " $CYAN %-6s$RESET $RED%s$RESET\n" "${name}:" "probe failed" + printf "%s\n" "$output" >&2 + if [ "$required" = "1" ]; then + exit 1 + fi + return 0 + fi + + version="$(printf '\''%s\n'\'' "$output" | head -n 1 | sed -E '\''s/^[[:space:]]+//; s/[[:space:]]+$//'\'')" + if [ -z "$version" ]; then + printf " $CYAN %-6s$RESET $RED%s$RESET\n" "${name}:" "empty version" + printf "%s\n" "$output" >&2 + if [ "$required" = "1" ]; then + exit 1 + fi + return 0 + fi + + printf " $CYAN %-6s$RESET %s%s$RESET\n" "${name}:" "$color" "$version" +} + + + + + +printf "\n$GREEN 🚀 Dev shell ready$RESET\n\n" +repo_lib_probe_tool \ + Bun \ + YELLOW \ + 1 \ + 1 \ + 0 \ + '\'''\'' \ + /nix/store/7x4dmbrjq24hgpzs1g6jxi3gm207ypkd-bun-1.3.10/bin/bun \ + --version +repo_lib_probe_tool \ + Go \ + CYAN \ + 1 \ + 1 \ + 0 \ + '\'''\'' \ + /nix/store/5ajixjk279m40yf6x96xxlnvw1wg6hq3-go-1.26.0/bin/go \ + version +repo_lib_probe_tool \ + Bazel \ + BLUE \ + 1 \ + 1 \ + 0 \ + '\'''\'' \ + /nix/store/xdyrcv7zr12n61nz4fzjf5knix0nj0xx-bazel/bin/bazel \ + --version +repo_lib_probe_tool \ + Wails \ + MAGENTA \ + 1 \ + 1 \ + 0 \ + '\'''\'' \ + /nix/store/g3r24xyhs1k9yba5166521fvvpr5rw77-wails3-3.0.0-alpha.74/bin/wails3 \ + version + +printf "\n" + +export USE_BAZEL_VERSION="${USE_BAZEL_VERSION:-9.0.0}" +export BUN_INSTALL="${BUN_INSTALL:-$HOME/.bun}" +export PATH="$BUN_INSTALL/bin:$PATH" + +' +export shellHook +stdenv='/nix/store/nssnpv7ghq4f1fdm41q304dmak5aanjv-stdenv-darwin' +export stdenv +strictDeps='' +export strictDeps +stripDebugFlags='-S' +system='aarch64-darwin' +export system +declare -a unpackCmdHooks=('_defaultUnpack' ) +_activatePkgs () +{ + + local hostOffset targetOffset; + local pkg; + for hostOffset in "${allPlatOffsets[@]}"; + do + local pkgsVar="${pkgAccumVarVars[hostOffset + 1]}"; + for targetOffset in "${allPlatOffsets[@]}"; + do + (( hostOffset <= targetOffset )) || continue; + local pkgsRef="${pkgsVar}[$targetOffset - $hostOffset]"; + local pkgsSlice="${!pkgsRef}[@]"; + for pkg in ${!pkgsSlice+"${!pkgsSlice}"}; + do + activatePackage "$pkg" "$hostOffset" "$targetOffset"; + done; + done; + done +} +_addRpathPrefix () +{ + + if [ "${NIX_NO_SELF_RPATH:-0}" != 1 ]; then + export NIX_LDFLAGS="-rpath $1/lib ${NIX_LDFLAGS-}"; + fi +} +_addToEnv () +{ + + local depHostOffset depTargetOffset; + local pkg; + for depHostOffset in "${allPlatOffsets[@]}"; + do + local hookVar="${pkgHookVarVars[depHostOffset + 1]}"; + local pkgsVar="${pkgAccumVarVars[depHostOffset + 1]}"; + for depTargetOffset in "${allPlatOffsets[@]}"; + do + (( depHostOffset <= depTargetOffset )) || continue; + local hookRef="${hookVar}[$depTargetOffset - $depHostOffset]"; + if [[ -z "${strictDeps-}" ]]; then + local visitedPkgs=""; + for pkg in "${pkgsBuildBuild[@]}" "${pkgsBuildHost[@]}" "${pkgsBuildTarget[@]}" "${pkgsHostHost[@]}" "${pkgsHostTarget[@]}" "${pkgsTargetTarget[@]}"; + do + if [[ "$visitedPkgs" = *"$pkg"* ]]; then + continue; + fi; + runHook "${!hookRef}" "$pkg"; + visitedPkgs+=" $pkg"; + done; + else + local pkgsRef="${pkgsVar}[$depTargetOffset - $depHostOffset]"; + local pkgsSlice="${!pkgsRef}[@]"; + for pkg in ${!pkgsSlice+"${!pkgsSlice}"}; + do + runHook "${!hookRef}" "$pkg"; + done; + fi; + done; + done +} +_allFlags () +{ + + export system pname name version; + while IFS='' read -r varName; do + nixTalkativeLog "@${varName}@ -> ${!varName}"; + args+=("--subst-var" "$varName"); + done < <(awk 'BEGIN { for (v in ENVIRON) if (v ~ /^[a-z][a-zA-Z0-9_]*$/) print v }') +} +_assignFirst () +{ + + local varName="$1"; + local _var; + local REMOVE=REMOVE; + shift; + for _var in "$@"; + do + if [ -n "${!_var-}" ]; then + eval "${varName}"="${_var}"; + return; + fi; + done; + echo; + echo "error: _assignFirst: could not find a non-empty variable whose name to assign to ${varName}."; + echo " The following variables were all unset or empty:"; + echo " $*"; + if [ -z "${out:-}" ]; then + echo ' If you do not want an "out" output in your derivation, make sure to define'; + echo ' the other specific required outputs. This can be achieved by picking one'; + echo " of the above as an output."; + echo ' You do not have to remove "out" if you want to have a different default'; + echo ' output, because the first output is taken as a default.'; + echo; + fi; + return 1 +} +_callImplicitHook () +{ + + local def="$1"; + local hookName="$2"; + if declare -F "$hookName" > /dev/null; then + nixTalkativeLog "calling implicit '$hookName' function hook"; + "$hookName"; + else + if type -p "$hookName" > /dev/null; then + nixTalkativeLog "sourcing implicit '$hookName' script hook"; + source "$hookName"; + else + if [ -n "${!hookName:-}" ]; then + nixTalkativeLog "evaling implicit '$hookName' string hook"; + eval "${!hookName}"; + else + return "$def"; + fi; + fi; + fi +} +_defaultUnpack () +{ + + local fn="$1"; + local destination; + if [ -d "$fn" ]; then + destination="$(stripHash "$fn")"; + if [ -e "$destination" ]; then + echo "Cannot copy $fn to $destination: destination already exists!"; + echo "Did you specify two \"srcs\" with the same \"name\"?"; + return 1; + fi; + cp -r --preserve=timestamps --reflink=auto -- "$fn" "$destination"; + else + case "$fn" in + *.tar.xz | *.tar.lzma | *.txz) + ( XZ_OPT="--threads=$NIX_BUILD_CORES" xz -d < "$fn"; + true ) | tar xf - --mode=+w --warning=no-timestamp + ;; + *.tar | *.tar.* | *.tgz | *.tbz2 | *.tbz) + tar xf "$fn" --mode=+w --warning=no-timestamp + ;; + *) + return 1 + ;; + esac; + fi +} +_doStrip () +{ + + local -ra flags=(dontStripHost dontStripTarget); + local -ra debugDirs=(stripDebugList stripDebugListTarget); + local -ra allDirs=(stripAllList stripAllListTarget); + local -ra stripCmds=(STRIP STRIP_FOR_TARGET); + local -ra ranlibCmds=(RANLIB RANLIB_FOR_TARGET); + stripDebugList=${stripDebugList[*]:-lib lib32 lib64 libexec bin sbin Applications Library/Frameworks}; + stripDebugListTarget=${stripDebugListTarget[*]:-}; + stripAllList=${stripAllList[*]:-}; + stripAllListTarget=${stripAllListTarget[*]:-}; + local i; + for i in ${!stripCmds[@]}; + do + local -n flag="${flags[$i]}"; + local -n debugDirList="${debugDirs[$i]}"; + local -n allDirList="${allDirs[$i]}"; + local -n stripCmd="${stripCmds[$i]}"; + local -n ranlibCmd="${ranlibCmds[$i]}"; + if [[ -n "${dontStrip-}" || -n "${flag-}" ]] || ! type -f "${stripCmd-}" 2> /dev/null 1>&2; then + continue; + fi; + stripDirs "$stripCmd" "$ranlibCmd" "$debugDirList" "${stripDebugFlags[*]:--S -p}"; + stripDirs "$stripCmd" "$ranlibCmd" "$allDirList" "${stripAllFlags[*]:--s -p}"; + done +} +_eval () +{ + + if declare -F "$1" > /dev/null 2>&1; then + "$@"; + else + eval "$1"; + fi +} +_logHook () +{ + + if [[ -z ${NIX_LOG_FD-} ]]; then + return; + fi; + local hookKind="$1"; + local hookExpr="$2"; + shift 2; + if declare -F "$hookExpr" > /dev/null 2>&1; then + nixTalkativeLog "calling '$hookKind' function hook '$hookExpr'" "$@"; + else + if type -p "$hookExpr" > /dev/null; then + nixTalkativeLog "sourcing '$hookKind' script hook '$hookExpr'"; + else + if [[ "$hookExpr" != "_callImplicitHook"* ]]; then + local exprToOutput; + if [[ ${NIX_DEBUG:-0} -ge 5 ]]; then + exprToOutput="$hookExpr"; + else + local hookExprLine; + while IFS= read -r hookExprLine; do + hookExprLine="${hookExprLine#"${hookExprLine%%[![:space:]]*}"}"; + if [[ -n "$hookExprLine" ]]; then + exprToOutput+="$hookExprLine\\n "; + fi; + done <<< "$hookExpr"; + exprToOutput="${exprToOutput%%\\n }"; + fi; + nixTalkativeLog "evaling '$hookKind' string hook '$exprToOutput'"; + fi; + fi; + fi +} +_makeSymlinksRelative () +{ + + local prefixes; + prefixes=(); + for output in $(getAllOutputNames); + do + [ ! -e "${!output}" ] && continue; + prefixes+=("${!output}"); + done; + find "${prefixes[@]}" -type l -printf '%H\0%p\0' | xargs -0 -n2 -r -P "$NIX_BUILD_CORES" sh -c ' + output="$1" + link="$2" + + linkTarget=$(readlink "$link") + + # only touch links that point inside the same output tree + [[ $linkTarget == "$output"/* ]] || exit 0 + + if [ ! -e "$linkTarget" ]; then + echo "the symlink $link is broken, it points to $linkTarget (which is missing)" + fi + + echo "making symlink relative: $link" + ln -snrf "$linkTarget" "$link" + ' _ +} +_moveLib64 () +{ + + if [ "${dontMoveLib64-}" = 1 ]; then + return; + fi; + if [ ! -e "$prefix/lib64" -o -L "$prefix/lib64" ]; then + return; + fi; + echo "moving $prefix/lib64/* to $prefix/lib"; + mkdir -p $prefix/lib; + shopt -s dotglob; + for i in $prefix/lib64/*; + do + mv --no-clobber "$i" $prefix/lib; + done; + shopt -u dotglob; + rmdir $prefix/lib64; + ln -s lib $prefix/lib64 +} +_moveSbin () +{ + + if [ "${dontMoveSbin-}" = 1 ]; then + return; + fi; + if [ ! -e "$prefix/sbin" -o -L "$prefix/sbin" ]; then + return; + fi; + echo "moving $prefix/sbin/* to $prefix/bin"; + mkdir -p $prefix/bin; + shopt -s dotglob; + for i in $prefix/sbin/*; + do + mv "$i" $prefix/bin; + done; + shopt -u dotglob; + rmdir $prefix/sbin; + ln -s bin $prefix/sbin +} +_moveSystemdUserUnits () +{ + + if [ "${dontMoveSystemdUserUnits:-0}" = 1 ]; then + return; + fi; + if [ ! -e "${prefix:?}/lib/systemd/user" ]; then + return; + fi; + local source="$prefix/lib/systemd/user"; + local target="$prefix/share/systemd/user"; + echo "moving $source/* to $target"; + mkdir -p "$target"; + ( shopt -s dotglob; + for i in "$source"/*; + do + mv "$i" "$target"; + done ); + rmdir "$source"; + ln -s "$target" "$source" +} +_moveToShare () +{ + + if [ -n "$__structuredAttrs" ]; then + if [ -z "${forceShare-}" ]; then + forceShare=(man doc info); + fi; + else + forceShare=(${forceShare:-man doc info}); + fi; + if [[ -z "$out" ]]; then + return; + fi; + for d in "${forceShare[@]}"; + do + if [ -d "$out/$d" ]; then + if [ -d "$out/share/$d" ]; then + echo "both $d/ and share/$d/ exist!"; + else + echo "moving $out/$d to $out/share/$d"; + mkdir -p $out/share; + mv $out/$d $out/share/; + fi; + fi; + done +} +_multioutConfig () +{ + + if [ "$(getAllOutputNames)" = "out" ] || [ -z "${setOutputFlags-1}" ]; then + return; + fi; + if [ -z "${shareDocName:-}" ]; then + local confScript="${configureScript:-}"; + if [ -z "$confScript" ] && [ -x ./configure ]; then + confScript=./configure; + fi; + if [ -f "$confScript" ]; then + local shareDocName="$(sed -n "s/^PACKAGE_TARNAME='\(.*\)'$/\1/p" < "$confScript")"; + fi; + if [ -z "$shareDocName" ] || echo "$shareDocName" | grep -q '[^a-zA-Z0-9_-]'; then + shareDocName="$(echo "$name" | sed 's/-[^a-zA-Z].*//')"; + fi; + fi; + prependToVar configureFlags --bindir="${!outputBin}"/bin --sbindir="${!outputBin}"/sbin --includedir="${!outputInclude}"/include --mandir="${!outputMan}"/share/man --infodir="${!outputInfo}"/share/info --docdir="${!outputDoc}"/share/doc/"${shareDocName}" --libdir="${!outputLib}"/lib --libexecdir="${!outputLib}"/libexec --localedir="${!outputLib}"/share/locale; + prependToVar installFlags pkgconfigdir="${!outputDev}"/lib/pkgconfig m4datadir="${!outputDev}"/share/aclocal aclocaldir="${!outputDev}"/share/aclocal +} +_multioutDevs () +{ + + if [ "$(getAllOutputNames)" = "out" ] || [ -z "${moveToDev-1}" ]; then + return; + fi; + moveToOutput include "${!outputInclude}"; + moveToOutput lib/pkgconfig "${!outputDev}"; + moveToOutput share/pkgconfig "${!outputDev}"; + moveToOutput lib/cmake "${!outputDev}"; + moveToOutput share/aclocal "${!outputDev}"; + for f in "${!outputDev}"/{lib,share}/pkgconfig/*.pc; + do + echo "Patching '$f' includedir to output ${!outputInclude}"; + sed -i "/^includedir=/s,=\${prefix},=${!outputInclude}," "$f"; + done +} +_multioutDocs () +{ + + local REMOVE=REMOVE; + moveToOutput share/info "${!outputInfo}"; + moveToOutput share/doc "${!outputDoc}"; + moveToOutput share/gtk-doc "${!outputDevdoc}"; + moveToOutput share/devhelp/books "${!outputDevdoc}"; + moveToOutput share/man "${!outputMan}"; + moveToOutput share/man/man3 "${!outputDevman}" +} +_multioutPropagateDev () +{ + + if [ "$(getAllOutputNames)" = "out" ]; then + return; + fi; + local outputFirst; + for outputFirst in $(getAllOutputNames); + do + break; + done; + local propagaterOutput="$outputDev"; + if [ -z "$propagaterOutput" ]; then + propagaterOutput="$outputFirst"; + fi; + if [ -z "${propagatedBuildOutputs+1}" ]; then + local po_dirty="$outputBin $outputInclude $outputLib"; + set +o pipefail; + propagatedBuildOutputs=`echo "$po_dirty" | tr -s ' ' '\n' | grep -v -F "$propagaterOutput" | sort -u | tr '\n' ' ' `; + set -o pipefail; + fi; + if [ -z "$propagatedBuildOutputs" ]; then + return; + fi; + mkdir -p "${!propagaterOutput}"/nix-support; + for output in $propagatedBuildOutputs; + do + echo -n " ${!output}" >> "${!propagaterOutput}"/nix-support/propagated-build-inputs; + done +} +_nixLogWithLevel () +{ + + [[ -z ${NIX_LOG_FD-} || ${NIX_DEBUG:-0} -lt ${1:?} ]] && return 0; + local logLevel; + case "${1:?}" in + 0) + logLevel=ERROR + ;; + 1) + logLevel=WARN + ;; + 2) + logLevel=NOTICE + ;; + 3) + logLevel=INFO + ;; + 4) + logLevel=TALKATIVE + ;; + 5) + logLevel=CHATTY + ;; + 6) + logLevel=DEBUG + ;; + 7) + logLevel=VOMIT + ;; + *) + echo "_nixLogWithLevel: called with invalid log level: ${1:?}" >&"$NIX_LOG_FD"; + return 1 + ;; + esac; + local callerName="${FUNCNAME[2]}"; + if [[ $callerName == "_callImplicitHook" ]]; then + callerName="${hookName:?}"; + fi; + printf "%s: %s: %s\n" "$logLevel" "$callerName" "${2:?}" >&"$NIX_LOG_FD" +} +_overrideFirst () +{ + + if [ -z "${!1-}" ]; then + _assignFirst "$@"; + fi +} +_pruneLibtoolFiles () +{ + + if [ "${dontPruneLibtoolFiles-}" ] || [ ! -e "$prefix" ]; then + return; + fi; + find "$prefix" -type f -name '*.la' -exec grep -q '^# Generated by .*libtool' {} \; -exec grep -q "^old_library=''" {} \; -exec sed -i {} -e "/^dependency_libs='[^']/ c dependency_libs='' #pruned" \; +} +_updateSourceDateEpochFromSourceRoot () +{ + + if [ -n "$sourceRoot" ]; then + updateSourceDateEpoch "$sourceRoot"; + fi +} +activatePackage () +{ + + local pkg="$1"; + local -r hostOffset="$2"; + local -r targetOffset="$3"; + (( hostOffset <= targetOffset )) || exit 1; + if [ -f "$pkg" ]; then + nixTalkativeLog "sourcing setup hook '$pkg'"; + source "$pkg"; + fi; + if [[ -z "${strictDeps-}" || "$hostOffset" -le -1 ]]; then + addToSearchPath _PATH "$pkg/bin"; + fi; + if (( hostOffset <= -1 )); then + addToSearchPath _XDG_DATA_DIRS "$pkg/share"; + fi; + if [[ "$hostOffset" -eq 0 && -d "$pkg/bin" ]]; then + addToSearchPath _HOST_PATH "$pkg/bin"; + fi; + if [[ -f "$pkg/nix-support/setup-hook" ]]; then + nixTalkativeLog "sourcing setup hook '$pkg/nix-support/setup-hook'"; + source "$pkg/nix-support/setup-hook"; + fi +} +addEnvHooks () +{ + + local depHostOffset="$1"; + shift; + local pkgHookVarsSlice="${pkgHookVarVars[$depHostOffset + 1]}[@]"; + local pkgHookVar; + for pkgHookVar in "${!pkgHookVarsSlice}"; + do + eval "${pkgHookVar}s"'+=("$@")'; + done +} +addPythonPath () +{ + + addToSearchPathWithCustomDelimiter : PYTHONPATH $1/lib/python3.13/site-packages +} +addToSearchPath () +{ + + addToSearchPathWithCustomDelimiter ":" "$@" +} +addToSearchPathWithCustomDelimiter () +{ + + local delimiter="$1"; + local varName="$2"; + local dir="$3"; + if [[ -d "$dir" && "${!varName:+${delimiter}${!varName}${delimiter}}" != *"${delimiter}${dir}${delimiter}"* ]]; then + export "${varName}=${!varName:+${!varName}${delimiter}}${dir}"; + fi +} +appendToVar () +{ + + local -n nameref="$1"; + local useArray type; + if [ -n "$__structuredAttrs" ]; then + useArray=true; + else + useArray=false; + fi; + if type=$(declare -p "$1" 2> /dev/null); then + case "${type#* }" in + -A*) + echo "appendToVar(): ERROR: trying to use appendToVar on an associative array, use variable+=([\"X\"]=\"Y\") instead." 1>&2; + return 1 + ;; + -a*) + useArray=true + ;; + *) + useArray=false + ;; + esac; + fi; + shift; + if $useArray; then + nameref=(${nameref+"${nameref[@]}"} "$@"); + else + nameref="${nameref-} $*"; + fi +} +auditTmpdir () +{ + + local dir="$1"; + [ -e "$dir" ] || return 0; + echo "checking for references to $TMPDIR/ in $dir..."; + local tmpdir elf_fifo script_fifo; + tmpdir="$(mktemp -d)"; + elf_fifo="$tmpdir/elf"; + script_fifo="$tmpdir/script"; + mkfifo "$elf_fifo" "$script_fifo"; + ( find "$dir" -type f -not -path '*/.build-id/*' -print0 | while IFS= read -r -d '' file; do + if isELF "$file"; then + printf '%s\0' "$file" 1>&3; + else + if isScript "$file"; then + filename=${file##*/}; + dir=${file%/*}; + if [ -e "$dir/.$filename-wrapped" ]; then + printf '%s\0' "$file" 1>&4; + fi; + fi; + fi; + done; + exec 3>&- 4>&- ) 3> "$elf_fifo" 4> "$script_fifo" & ( xargs -0 -r -P "$NIX_BUILD_CORES" -n 1 sh -c ' + if { printf :; patchelf --print-rpath "$1"; } | grep -q -F ":$TMPDIR/"; then + echo "RPATH of binary $1 contains a forbidden reference to $TMPDIR/" + exit 1 + fi + ' _ < "$elf_fifo" ) & local pid_elf=$!; + local pid_script; + ( xargs -0 -r -P "$NIX_BUILD_CORES" -n 1 sh -c ' + if grep -q -F "$TMPDIR/" "$1"; then + echo "wrapper script $1 contains a forbidden reference to $TMPDIR/" + exit 1 + fi + ' _ < "$script_fifo" ) & local pid_script=$!; + wait "$pid_elf" || { + echo "Some binaries contain forbidden references to $TMPDIR/. Check the error above!"; + exit 1 + }; + wait "$pid_script" || { + echo "Some scripts contain forbidden references to $TMPDIR/. Check the error above!"; + exit 1 + }; + rm -r "$tmpdir" +} +bintoolsWrapper_addLDVars () +{ + + local role_post; + getHostRoleEnvHook; + if [[ -d "$1/lib64" && ! -L "$1/lib64" ]]; then + export NIX_LDFLAGS${role_post}+=" -L$1/lib64"; + fi; + if [[ -d "$1/lib" ]]; then + local -a glob=($1/lib/lib*); + if [ "${#glob[*]}" -gt 0 ]; then + export NIX_LDFLAGS${role_post}+=" -L$1/lib"; + fi; + fi +} +buildPhase () +{ + + runHook preBuild; + if [[ -z "${makeFlags-}" && -z "${makefile:-}" && ! ( -e Makefile || -e makefile || -e GNUmakefile ) ]]; then + echo "no Makefile or custom buildPhase, doing nothing"; + else + foundMakefile=1; + local flagsArray=(${enableParallelBuilding:+-j${NIX_BUILD_CORES}} SHELL="$SHELL"); + concatTo flagsArray makeFlags makeFlagsArray buildFlags buildFlagsArray; + echoCmd 'build flags' "${flagsArray[@]}"; + make ${makefile:+-f $makefile} "${flagsArray[@]}"; + unset flagsArray; + fi; + runHook postBuild +} +ccWrapper_addCVars () +{ + + local role_post; + getHostRoleEnvHook; + local found=; + if [ -d "$1/include" ]; then + export NIX_CFLAGS_COMPILE${role_post}+=" -isystem $1/include"; + found=1; + fi; + if [ -d "$1/Library/Frameworks" ]; then + export NIX_CFLAGS_COMPILE${role_post}+=" -iframework $1/Library/Frameworks"; + found=1; + fi; + if [[ -n "1" && -n ${NIX_STORE:-} && -n $found ]]; then + local scrubbed="$NIX_STORE/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-${1#"$NIX_STORE"/*-}"; + export NIX_CFLAGS_COMPILE${role_post}+=" -fmacro-prefix-map=$1=$scrubbed"; + fi +} +checkPhase () +{ + + runHook preCheck; + if [[ -z "${foundMakefile:-}" ]]; then + echo "no Makefile or custom checkPhase, doing nothing"; + runHook postCheck; + return; + fi; + if [[ -z "${checkTarget:-}" ]]; then + if make -n ${makefile:+-f $makefile} check > /dev/null 2>&1; then + checkTarget="check"; + else + if make -n ${makefile:+-f $makefile} test > /dev/null 2>&1; then + checkTarget="test"; + fi; + fi; + fi; + if [[ -z "${checkTarget:-}" ]]; then + echo "no check/test target in ${makefile:-Makefile}, doing nothing"; + else + local flagsArray=(${enableParallelChecking:+-j${NIX_BUILD_CORES}} SHELL="$SHELL"); + concatTo flagsArray makeFlags makeFlagsArray checkFlags=VERBOSE=y checkFlagsArray checkTarget; + echoCmd 'check flags' "${flagsArray[@]}"; + make ${makefile:+-f $makefile} "${flagsArray[@]}"; + unset flagsArray; + fi; + runHook postCheck +} +compressManPages () +{ + + local dir="$1"; + if [ -L "$dir"/share ] || [ -L "$dir"/share/man ] || [ ! -d "$dir/share/man" ]; then + return; + fi; + echo "gzipping man pages under $dir/share/man/"; + find "$dir"/share/man/ -type f -a '!' -regex '.*\.\(bz2\|gz\|xz\)$' -print0 | xargs -0 -n1 -P "$NIX_BUILD_CORES" gzip -n -f; + find "$dir"/share/man/ -type l -a '!' -regex '.*\.\(bz2\|gz\|xz\)$' -print0 | sort -z | while IFS= read -r -d '' f; do + local target; + target="$(readlink -f "$f")"; + if [ -f "$target".gz ]; then + ln -sf "$target".gz "$f".gz && rm "$f"; + fi; + done +} +concatStringsSep () +{ + + local sep="$1"; + local name="$2"; + local type oldifs; + if type=$(declare -p "$name" 2> /dev/null); then + local -n nameref="$name"; + case "${type#* }" in + -A*) + echo "concatStringsSep(): ERROR: trying to use concatStringsSep on an associative array." 1>&2; + return 1 + ;; + -a*) + local IFS="$(printf '\036')" + ;; + *) + local IFS=" " + ;; + esac; + local ifs_separated="${nameref[*]}"; + echo -n "${ifs_separated//"$IFS"/"$sep"}"; + fi +} +concatTo () +{ + + local -; + set -o noglob; + local -n targetref="$1"; + shift; + local arg default name type; + for arg in "$@"; + do + IFS="=" read -r name default <<< "$arg"; + local -n nameref="$name"; + if [[ -z "${nameref[*]}" && -n "$default" ]]; then + targetref+=("$default"); + else + if type=$(declare -p "$name" 2> /dev/null); then + case "${type#* }" in + -A*) + echo "concatTo(): ERROR: trying to use concatTo on an associative array." 1>&2; + return 1 + ;; + -a*) + targetref+=("${nameref[@]}") + ;; + *) + if [[ "$name" = *"Array" ]]; then + nixErrorLog "concatTo(): $name is not declared as array, treating as a singleton. This will become an error in future"; + targetref+=(${nameref+"${nameref[@]}"}); + else + targetref+=(${nameref-}); + fi + ;; + esac; + fi; + fi; + done +} +configurePhase () +{ + + runHook preConfigure; + : "${configureScript=}"; + if [[ -z "$configureScript" && -x ./configure ]]; then + configureScript=./configure; + fi; + if [ -z "${dontFixLibtool:-}" ]; then + export lt_cv_deplibs_check_method="${lt_cv_deplibs_check_method-pass_all}"; + local i; + find . -iname "ltmain.sh" -print0 | while IFS='' read -r -d '' i; do + echo "fixing libtool script $i"; + fixLibtool "$i"; + done; + CONFIGURE_MTIME_REFERENCE=$(mktemp configure.mtime.reference.XXXXXX); + find . -executable -type f -name configure -exec grep -l 'GNU Libtool is free software; you can redistribute it and/or modify' {} \; -exec touch -r {} "$CONFIGURE_MTIME_REFERENCE" \; -exec sed -i s_/usr/bin/file_file_g {} \; -exec touch -r "$CONFIGURE_MTIME_REFERENCE" {} \;; + rm -f "$CONFIGURE_MTIME_REFERENCE"; + fi; + if [[ -z "${dontAddPrefix:-}" && -n "$prefix" ]]; then + local -r prefixKeyOrDefault="${prefixKey:---prefix=}"; + if [ "${prefixKeyOrDefault: -1}" = " " ]; then + prependToVar configureFlags "$prefix"; + prependToVar configureFlags "${prefixKeyOrDefault::-1}"; + else + prependToVar configureFlags "$prefixKeyOrDefault$prefix"; + fi; + fi; + if [[ -f "$configureScript" ]]; then + if [ -z "${dontAddDisableDepTrack:-}" ]; then + if grep -q dependency-tracking "$configureScript"; then + prependToVar configureFlags --disable-dependency-tracking; + fi; + fi; + if [ -z "${dontDisableStatic:-}" ]; then + if grep -q enable-static "$configureScript"; then + prependToVar configureFlags --disable-static; + fi; + fi; + if [ -z "${dontPatchShebangsInConfigure:-}" ]; then + patchShebangs --build "$configureScript"; + fi; + fi; + if [ -n "$configureScript" ]; then + local -a flagsArray; + concatTo flagsArray configureFlags configureFlagsArray; + echoCmd 'configure flags' "${flagsArray[@]}"; + $configureScript "${flagsArray[@]}"; + unset flagsArray; + else + echo "no configure script, doing nothing"; + fi; + runHook postConfigure +} +consumeEntire () +{ + + if IFS='' read -r -d '' "$1"; then + echo "consumeEntire(): ERROR: Input null bytes, won't process" 1>&2; + return 1; + fi +} +definePhases () +{ + + if [ -z "${phases[*]:-}" ]; then + phases="${prePhases[*]:-} unpackPhase patchPhase ${preConfigurePhases[*]:-} configurePhase ${preBuildPhases[*]:-} buildPhase checkPhase ${preInstallPhases[*]:-} installPhase ${preFixupPhases[*]:-} fixupPhase installCheckPhase ${preDistPhases[*]:-} distPhase ${postPhases[*]:-}"; + fi +} +distPhase () +{ + + runHook preDist; + local flagsArray=(); + concatTo flagsArray distFlags distFlagsArray distTarget=dist; + echo 'dist flags: %q' "${flagsArray[@]}"; + make ${makefile:+-f $makefile} "${flagsArray[@]}"; + if [ "${dontCopyDist:-0}" != 1 ]; then + mkdir -p "$out/tarballs"; + cp -pvd ${tarballs[*]:-*.tar.gz} "$out/tarballs"; + fi; + runHook postDist +} +dumpVars () +{ + + if [[ "${noDumpEnvVars:-0}" != 1 && -d "$NIX_BUILD_TOP" ]]; then + local old_umask; + old_umask=$(umask); + umask 0077; + export 2> /dev/null > "$NIX_BUILD_TOP/env-vars"; + umask "$old_umask"; + fi +} +echoCmd () +{ + + printf "%s:" "$1"; + shift; + printf ' %q' "$@"; + echo +} +exitHandler () +{ + + exitCode="$?"; + set +e; + if [ -n "${showBuildStats:-}" ]; then + read -r -d '' -a buildTimes < <(times); + echo "build times:"; + echo "user time for the shell ${buildTimes[0]}"; + echo "system time for the shell ${buildTimes[1]}"; + echo "user time for all child processes ${buildTimes[2]}"; + echo "system time for all child processes ${buildTimes[3]}"; + fi; + if (( "$exitCode" != 0 )); then + runHook failureHook; + if [ -n "${succeedOnFailure:-}" ]; then + echo "build failed with exit code $exitCode (ignored)"; + mkdir -p "$out/nix-support"; + printf "%s" "$exitCode" > "$out/nix-support/failed"; + exit 0; + fi; + else + runHook exitHook; + fi; + return "$exitCode" +} +findInputs () +{ + + local -r pkg="$1"; + local -r hostOffset="$2"; + local -r targetOffset="$3"; + (( hostOffset <= targetOffset )) || exit 1; + local varVar="${pkgAccumVarVars[hostOffset + 1]}"; + local varRef="$varVar[$((targetOffset - hostOffset))]"; + local var="${!varRef}"; + unset -v varVar varRef; + local varSlice="$var[*]"; + case " ${!varSlice-} " in + *" $pkg "*) + return 0 + ;; + esac; + unset -v varSlice; + eval "$var"'+=("$pkg")'; + if ! [ -e "$pkg" ]; then + echo "build input $pkg does not exist" 1>&2; + exit 1; + fi; + function mapOffset () + { + local -r inputOffset="$1"; + local -n outputOffset="$2"; + if (( inputOffset <= 0 )); then + outputOffset=$((inputOffset + hostOffset)); + else + outputOffset=$((inputOffset - 1 + targetOffset)); + fi + }; + local relHostOffset; + for relHostOffset in "${allPlatOffsets[@]}"; + do + local files="${propagatedDepFilesVars[relHostOffset + 1]}"; + local hostOffsetNext; + mapOffset "$relHostOffset" hostOffsetNext; + (( -1 <= hostOffsetNext && hostOffsetNext <= 1 )) || continue; + local relTargetOffset; + for relTargetOffset in "${allPlatOffsets[@]}"; + do + (( "$relHostOffset" <= "$relTargetOffset" )) || continue; + local fileRef="${files}[$relTargetOffset - $relHostOffset]"; + local file="${!fileRef}"; + unset -v fileRef; + local targetOffsetNext; + mapOffset "$relTargetOffset" targetOffsetNext; + (( -1 <= hostOffsetNext && hostOffsetNext <= 1 )) || continue; + [[ -f "$pkg/nix-support/$file" ]] || continue; + local pkgNext; + read -r -d '' pkgNext < "$pkg/nix-support/$file" || true; + for pkgNext in $pkgNext; + do + findInputs "$pkgNext" "$hostOffsetNext" "$targetOffsetNext"; + done; + done; + done +} +fixLibtool () +{ + + local search_path; + for flag in $NIX_LDFLAGS; + do + case $flag in + -L*) + search_path+=" ${flag#-L}" + ;; + esac; + done; + sed -i "$1" -e "s^eval \(sys_lib_search_path=\).*^\1'${search_path:-}'^" -e 's^eval sys_lib_.+search_path=.*^^' +} +fixupPhase () +{ + + local output; + for output in $(getAllOutputNames); + do + if [ -e "${!output}" ]; then + chmod -R u+w,u-s,g-s "${!output}"; + fi; + done; + runHook preFixup; + local output; + for output in $(getAllOutputNames); + do + prefix="${!output}" runHook fixupOutput; + done; + recordPropagatedDependencies; + if [ -n "${setupHook:-}" ]; then + mkdir -p "${!outputDev}/nix-support"; + substituteAll "$setupHook" "${!outputDev}/nix-support/setup-hook"; + fi; + if [ -n "${setupHooks:-}" ]; then + mkdir -p "${!outputDev}/nix-support"; + local hook; + for hook in ${setupHooks[@]}; + do + local content; + consumeEntire content < "$hook"; + substituteAllStream content "file '$hook'" >> "${!outputDev}/nix-support/setup-hook"; + unset -v content; + done; + unset -v hook; + fi; + if [ -n "${propagatedUserEnvPkgs[*]:-}" ]; then + mkdir -p "${!outputBin}/nix-support"; + printWords "${propagatedUserEnvPkgs[@]}" > "${!outputBin}/nix-support/propagated-user-env-packages"; + fi; + runHook postFixup +} +genericBuild () +{ + + export GZIP_NO_TIMESTAMPS=1; + if [ -f "${buildCommandPath:-}" ]; then + source "$buildCommandPath"; + return; + fi; + if [ -n "${buildCommand:-}" ]; then + eval "$buildCommand"; + return; + fi; + definePhases; + for curPhase in ${phases[*]}; + do + runPhase "$curPhase"; + done +} +getAllOutputNames () +{ + + if [ -n "$__structuredAttrs" ]; then + echo "${!outputs[*]}"; + else + echo "$outputs"; + fi +} +getHostRole () +{ + + getRole "$hostOffset" +} +getHostRoleEnvHook () +{ + + getRole "$depHostOffset" +} +getRole () +{ + + case $1 in + -1) + role_post='_FOR_BUILD' + ;; + 0) + role_post='' + ;; + 1) + role_post='_FOR_TARGET' + ;; + *) + echo "apple-sdk-14.4: used as improper sort of dependency" 1>&2; + return 1 + ;; + esac +} +getTargetRole () +{ + + getRole "$targetOffset" +} +getTargetRoleEnvHook () +{ + + getRole "$depTargetOffset" +} +getTargetRoleWrapper () +{ + + case $targetOffset in + -1) + export NIX_@wrapperName@_TARGET_BUILD_@suffixSalt@=1 + ;; + 0) + export NIX_@wrapperName@_TARGET_HOST_@suffixSalt@=1 + ;; + 1) + export NIX_@wrapperName@_TARGET_TARGET_@suffixSalt@=1 + ;; + *) + echo "apple-sdk-14.4: used as improper sort of dependency" 1>&2; + return 1 + ;; + esac +} +installCheckPhase () +{ + + runHook preInstallCheck; + if [[ -z "${foundMakefile:-}" ]]; then + echo "no Makefile or custom installCheckPhase, doing nothing"; + else + if [[ -z "${installCheckTarget:-}" ]] && ! make -n ${makefile:+-f $makefile} "${installCheckTarget:-installcheck}" > /dev/null 2>&1; then + echo "no installcheck target in ${makefile:-Makefile}, doing nothing"; + else + local flagsArray=(${enableParallelChecking:+-j${NIX_BUILD_CORES}} SHELL="$SHELL"); + concatTo flagsArray makeFlags makeFlagsArray installCheckFlags installCheckFlagsArray installCheckTarget=installcheck; + echoCmd 'installcheck flags' "${flagsArray[@]}"; + make ${makefile:+-f $makefile} "${flagsArray[@]}"; + unset flagsArray; + fi; + fi; + runHook postInstallCheck +} +installPhase () +{ + + runHook preInstall; + if [[ -z "${makeFlags-}" && -z "${makefile:-}" && ! ( -e Makefile || -e makefile || -e GNUmakefile ) ]]; then + echo "no Makefile or custom installPhase, doing nothing"; + runHook postInstall; + return; + else + foundMakefile=1; + fi; + if [ -n "$prefix" ]; then + mkdir -p "$prefix"; + fi; + local flagsArray=(${enableParallelInstalling:+-j${NIX_BUILD_CORES}} SHELL="$SHELL"); + concatTo flagsArray makeFlags makeFlagsArray installFlags installFlagsArray installTargets=install; + echoCmd 'install flags' "${flagsArray[@]}"; + make ${makefile:+-f $makefile} "${flagsArray[@]}"; + unset flagsArray; + runHook postInstall +} +isELF () +{ + + local fn="$1"; + local fd; + local magic; + exec {fd}< "$fn"; + LANG=C read -r -n 4 -u "$fd" magic; + exec {fd}>&-; + if [ "$magic" = 'ELF' ]; then + return 0; + else + return 1; + fi +} +isMachO () +{ + + local fn="$1"; + local fd; + local magic; + exec {fd}< "$fn"; + LANG=C read -r -n 4 -u "$fd" magic; + exec {fd}>&-; + if [[ "$magic" = $(echo -ne "\xfe\xed\xfa\xcf") || "$magic" = $(echo -ne "\xcf\xfa\xed\xfe") ]]; then + return 0; + else + if [[ "$magic" = $(echo -ne "\xfe\xed\xfa\xce") || "$magic" = $(echo -ne "\xce\xfa\xed\xfe") ]]; then + return 0; + else + if [[ "$magic" = $(echo -ne "\xca\xfe\xba\xbe") || "$magic" = $(echo -ne "\xbe\xba\xfe\xca") ]]; then + return 0; + else + return 1; + fi; + fi; + fi +} +isScript () +{ + + local fn="$1"; + local fd; + local magic; + exec {fd}< "$fn"; + LANG=C read -r -n 2 -u "$fd" magic; + exec {fd}>&-; + if [[ "$magic" =~ \#! ]]; then + return 0; + else + return 1; + fi +} +mapOffset () +{ + + local -r inputOffset="$1"; + local -n outputOffset="$2"; + if (( inputOffset <= 0 )); then + outputOffset=$((inputOffset + hostOffset)); + else + outputOffset=$((inputOffset - 1 + targetOffset)); + fi +} +moveToOutput () +{ + + local patt="$1"; + local dstOut="$2"; + local output; + for output in $(getAllOutputNames); + do + if [ "${!output}" = "$dstOut" ]; then + continue; + fi; + local srcPath; + for srcPath in "${!output}"/$patt; + do + if [ ! -e "$srcPath" ] && [ ! -L "$srcPath" ]; then + continue; + fi; + if [ "$dstOut" = REMOVE ]; then + echo "Removing $srcPath"; + rm -r "$srcPath"; + else + local dstPath="$dstOut${srcPath#${!output}}"; + echo "Moving $srcPath to $dstPath"; + if [ -d "$dstPath" ] && [ -d "$srcPath" ]; then + rmdir "$srcPath" --ignore-fail-on-non-empty; + if [ -d "$srcPath" ]; then + mv -t "$dstPath" "$srcPath"/*; + rmdir "$srcPath"; + fi; + else + mkdir -p "$(readlink -m "$dstPath/..")"; + mv "$srcPath" "$dstPath"; + fi; + fi; + local srcParent="$(readlink -m "$srcPath/..")"; + if [ -n "$(find "$srcParent" -maxdepth 0 -type d -empty 2> /dev/null)" ]; then + echo "Removing empty $srcParent/ and (possibly) its parents"; + rmdir -p --ignore-fail-on-non-empty "$srcParent" 2> /dev/null || true; + fi; + done; + done +} +nixChattyLog () +{ + + _nixLogWithLevel 5 "$*" +} +nixDebugLog () +{ + + _nixLogWithLevel 6 "$*" +} +nixErrorLog () +{ + + _nixLogWithLevel 0 "$*" +} +nixInfoLog () +{ + + _nixLogWithLevel 3 "$*" +} +nixLog () +{ + + [[ -z ${NIX_LOG_FD-} ]] && return 0; + local callerName="${FUNCNAME[1]}"; + if [[ $callerName == "_callImplicitHook" ]]; then + callerName="${hookName:?}"; + fi; + printf "%s: %s\n" "$callerName" "$*" >&"$NIX_LOG_FD" +} +nixNoticeLog () +{ + + _nixLogWithLevel 2 "$*" +} +nixTalkativeLog () +{ + + _nixLogWithLevel 4 "$*" +} +nixVomitLog () +{ + + _nixLogWithLevel 7 "$*" +} +nixWarnLog () +{ + + _nixLogWithLevel 1 "$*" +} +noBrokenSymlinks () +{ + + local -r output="${1:?}"; + local path; + local pathParent; + local symlinkTarget; + local -i numDanglingSymlinks=0; + local -i numReflexiveSymlinks=0; + local -i numUnreadableSymlinks=0; + if [[ ! -e $output ]]; then + nixWarnLog "skipping non-existent output $output"; + return 0; + fi; + nixInfoLog "running on $output"; + while IFS= read -r -d '' path; do + pathParent="$(dirname "$path")"; + if ! symlinkTarget="$(readlink "$path")"; then + nixErrorLog "the symlink $path is unreadable"; + numUnreadableSymlinks+=1; + continue; + fi; + if [[ $symlinkTarget == /* ]]; then + nixInfoLog "symlink $path points to absolute target $symlinkTarget"; + else + nixInfoLog "symlink $path points to relative target $symlinkTarget"; + symlinkTarget="$(realpath --no-symlinks --canonicalize-missing "$pathParent/$symlinkTarget")"; + fi; + if [[ $symlinkTarget = "$TMPDIR"/* ]]; then + nixErrorLog "the symlink $path points to $TMPDIR directory: $symlinkTarget"; + numDanglingSymlinks+=1; + continue; + fi; + if [[ $symlinkTarget != "$NIX_STORE"/* ]]; then + nixInfoLog "symlink $path points outside the Nix store; ignoring"; + continue; + fi; + if [[ $path == "$symlinkTarget" ]]; then + nixErrorLog "the symlink $path is reflexive"; + numReflexiveSymlinks+=1; + else + if [[ ! -e $symlinkTarget ]]; then + nixErrorLog "the symlink $path points to a missing target: $symlinkTarget"; + numDanglingSymlinks+=1; + else + nixDebugLog "the symlink $path is irreflexive and points to a target which exists"; + fi; + fi; + done < <(find "$output" -type l -print0); + if ((numDanglingSymlinks > 0 || numReflexiveSymlinks > 0 || numUnreadableSymlinks > 0)); then + nixErrorLog "found $numDanglingSymlinks dangling symlinks, $numReflexiveSymlinks reflexive symlinks and $numUnreadableSymlinks unreadable symlinks"; + exit 1; + fi; + return 0 +} +noBrokenSymlinksInAllOutputs () +{ + + if [[ -z ${dontCheckForBrokenSymlinks-} ]]; then + for output in $(getAllOutputNames); + do + noBrokenSymlinks "${!output}"; + done; + fi +} +patchPhase () +{ + + runHook prePatch; + local -a patchesArray; + concatTo patchesArray patches; + local -a flagsArray; + concatTo flagsArray patchFlags=-p1; + for i in "${patchesArray[@]}"; + do + echo "applying patch $i"; + local uncompress=cat; + case "$i" in + *.gz) + uncompress="gzip -d" + ;; + *.bz2) + uncompress="bzip2 -d" + ;; + *.xz) + uncompress="xz -d" + ;; + *.lzma) + uncompress="lzma -d" + ;; + esac; + $uncompress < "$i" 2>&1 | patch "${flagsArray[@]}"; + done; + runHook postPatch +} +patchShebangs () +{ + + local pathName; + local update=false; + while [[ $# -gt 0 ]]; do + case "$1" in + --host) + pathName=HOST_PATH; + shift + ;; + --build) + pathName=PATH; + shift + ;; + --update) + update=true; + shift + ;; + --) + shift; + break + ;; + -* | --*) + echo "Unknown option $1 supplied to patchShebangs" 1>&2; + return 1 + ;; + *) + break + ;; + esac; + done; + echo "patching script interpreter paths in $@"; + local f; + local oldPath; + local newPath; + local arg0; + local args; + local oldInterpreterLine; + local newInterpreterLine; + if [[ $# -eq 0 ]]; then + echo "No arguments supplied to patchShebangs" 1>&2; + return 0; + fi; + local f; + while IFS= read -r -d '' f; do + isScript "$f" || continue; + read -r oldInterpreterLine < "$f" || [ "$oldInterpreterLine" ]; + read -r oldPath arg0 args <<< "${oldInterpreterLine:2}"; + if [[ -z "${pathName:-}" ]]; then + if [[ -n $strictDeps && $f == "$NIX_STORE"* ]]; then + pathName=HOST_PATH; + else + pathName=PATH; + fi; + fi; + if [[ "$oldPath" == *"/bin/env" ]]; then + if [[ $arg0 == "-S" ]]; then + arg0=${args%% *}; + [[ "$args" == *" "* ]] && args=${args#* } || args=; + newPath="$(PATH="${!pathName}" type -P "env" || true)"; + args="-S $(PATH="${!pathName}" type -P "$arg0" || true) $args"; + else + if [[ $arg0 == "-"* || $arg0 == *"="* ]]; then + echo "$f: unsupported interpreter directive \"$oldInterpreterLine\" (set dontPatchShebangs=1 and handle shebang patching yourself)" 1>&2; + exit 1; + else + newPath="$(PATH="${!pathName}" type -P "$arg0" || true)"; + fi; + fi; + else + if [[ -z $oldPath ]]; then + oldPath="/bin/sh"; + fi; + newPath="$(PATH="${!pathName}" type -P "$(basename "$oldPath")" || true)"; + args="$arg0 $args"; + fi; + newInterpreterLine="$newPath $args"; + newInterpreterLine=${newInterpreterLine%${newInterpreterLine##*[![:space:]]}}; + if [[ -n "$oldPath" && ( "$update" == true || "${oldPath:0:${#NIX_STORE}}" != "$NIX_STORE" ) ]]; then + if [[ -n "$newPath" && "$newPath" != "$oldPath" ]]; then + echo "$f: interpreter directive changed from \"$oldInterpreterLine\" to \"$newInterpreterLine\""; + escapedInterpreterLine=${newInterpreterLine//\\/\\\\}; + timestamp=$(stat --printf "%y" "$f"); + tmpFile=$(mktemp -t patchShebangs.XXXXXXXXXX); + sed -e "1 s|.*|#\!$escapedInterpreterLine|" "$f" > "$tmpFile"; + local restoreReadOnly; + if [[ ! -w "$f" ]]; then + chmod +w "$f"; + restoreReadOnly=true; + fi; + cat "$tmpFile" > "$f"; + rm "$tmpFile"; + if [[ -n "${restoreReadOnly:-}" ]]; then + chmod -w "$f"; + fi; + touch --date "$timestamp" "$f"; + fi; + fi; + done < <(find "$@" -type f -perm -0100 -print0) +} +patchShebangsAuto () +{ + + if [[ -z "${dontPatchShebangs-}" && -e "$prefix" ]]; then + if [[ "$output" != out && "$output" = "$outputDev" ]]; then + patchShebangs --build "$prefix"; + else + patchShebangs --host "$prefix"; + fi; + fi +} +prependToVar () +{ + + local -n nameref="$1"; + local useArray type; + if [ -n "$__structuredAttrs" ]; then + useArray=true; + else + useArray=false; + fi; + if type=$(declare -p "$1" 2> /dev/null); then + case "${type#* }" in + -A*) + echo "prependToVar(): ERROR: trying to use prependToVar on an associative array." 1>&2; + return 1 + ;; + -a*) + useArray=true + ;; + *) + useArray=false + ;; + esac; + fi; + shift; + if $useArray; then + nameref=("$@" ${nameref+"${nameref[@]}"}); + else + nameref="$* ${nameref-}"; + fi +} +printLines () +{ + + (( "$#" > 0 )) || return 0; + printf '%s\n' "$@" +} +printPhases () +{ + + definePhases; + local phase; + for phase in ${phases[*]}; + do + printf '%s\n' "$phase"; + done +} +printWords () +{ + + (( "$#" > 0 )) || return 0; + printf '%s ' "$@" +} +recordPropagatedDependencies () +{ + + declare -ra flatVars=(depsBuildBuildPropagated propagatedNativeBuildInputs depsBuildTargetPropagated depsHostHostPropagated propagatedBuildInputs depsTargetTargetPropagated); + declare -ra flatFiles=("${propagatedBuildDepFiles[@]}" "${propagatedHostDepFiles[@]}" "${propagatedTargetDepFiles[@]}"); + local propagatedInputsIndex; + for propagatedInputsIndex in "${!flatVars[@]}"; + do + local propagatedInputsSlice="${flatVars[$propagatedInputsIndex]}[@]"; + local propagatedInputsFile="${flatFiles[$propagatedInputsIndex]}"; + [[ -n "${!propagatedInputsSlice}" ]] || continue; + mkdir -p "${!outputDev}/nix-support"; + printWords ${!propagatedInputsSlice} > "${!outputDev}/nix-support/$propagatedInputsFile"; + done +} +runHook () +{ + + local hookName="$1"; + shift; + local hooksSlice="${hookName%Hook}Hooks[@]"; + local hook; + for hook in "_callImplicitHook 0 $hookName" ${!hooksSlice+"${!hooksSlice}"}; + do + _logHook "$hookName" "$hook" "$@"; + _eval "$hook" "$@"; + done; + return 0 +} +runOneHook () +{ + + local hookName="$1"; + shift; + local hooksSlice="${hookName%Hook}Hooks[@]"; + local hook ret=1; + for hook in "_callImplicitHook 1 $hookName" ${!hooksSlice+"${!hooksSlice}"}; + do + _logHook "$hookName" "$hook" "$@"; + if _eval "$hook" "$@"; then + ret=0; + break; + fi; + done; + return "$ret" +} +runPhase () +{ + + local curPhase="$*"; + if [[ "$curPhase" = unpackPhase && -n "${dontUnpack:-}" ]]; then + return; + fi; + if [[ "$curPhase" = patchPhase && -n "${dontPatch:-}" ]]; then + return; + fi; + if [[ "$curPhase" = configurePhase && -n "${dontConfigure:-}" ]]; then + return; + fi; + if [[ "$curPhase" = buildPhase && -n "${dontBuild:-}" ]]; then + return; + fi; + if [[ "$curPhase" = checkPhase && -z "${doCheck:-}" ]]; then + return; + fi; + if [[ "$curPhase" = installPhase && -n "${dontInstall:-}" ]]; then + return; + fi; + if [[ "$curPhase" = fixupPhase && -n "${dontFixup:-}" ]]; then + return; + fi; + if [[ "$curPhase" = installCheckPhase && -z "${doInstallCheck:-}" ]]; then + return; + fi; + if [[ "$curPhase" = distPhase && -z "${doDist:-}" ]]; then + return; + fi; + showPhaseHeader "$curPhase"; + dumpVars; + local startTime endTime; + startTime=$(date +"%s"); + eval "${!curPhase:-$curPhase}"; + endTime=$(date +"%s"); + showPhaseFooter "$curPhase" "$startTime" "$endTime"; + if [ "$curPhase" = unpackPhase ]; then + [ -n "${sourceRoot:-}" ] && chmod +x -- "${sourceRoot}"; + cd -- "${sourceRoot:-.}"; + fi +} +showPhaseFooter () +{ + + local phase="$1"; + local startTime="$2"; + local endTime="$3"; + local delta=$(( endTime - startTime )); + (( delta < 30 )) && return; + local H=$((delta/3600)); + local M=$((delta%3600/60)); + local S=$((delta%60)); + echo -n "$phase completed in "; + (( H > 0 )) && echo -n "$H hours "; + (( M > 0 )) && echo -n "$M minutes "; + echo "$S seconds" +} +showPhaseHeader () +{ + + local phase="$1"; + echo "Running phase: $phase"; + if [[ -z ${NIX_LOG_FD-} ]]; then + return; + fi; + printf "@nix { \"action\": \"setPhase\", \"phase\": \"%s\" }\n" "$phase" >&"$NIX_LOG_FD" +} +stripDirs () +{ + + local cmd="$1"; + local ranlibCmd="$2"; + local paths="$3"; + local stripFlags="$4"; + local excludeFlags=(); + local pathsNew=; + [ -z "$cmd" ] && echo "stripDirs: Strip command is empty" 1>&2 && exit 1; + [ -z "$ranlibCmd" ] && echo "stripDirs: Ranlib command is empty" 1>&2 && exit 1; + local pattern; + if [ -n "${stripExclude:-}" ]; then + for pattern in "${stripExclude[@]}"; + do + excludeFlags+=(-a '!' '(' -name "$pattern" -o -wholename "$prefix/$pattern" ')'); + done; + fi; + local p; + for p in ${paths}; + do + if [ -e "$prefix/$p" ]; then + pathsNew="${pathsNew} $prefix/$p"; + fi; + done; + paths=${pathsNew}; + if [ -n "${paths}" ]; then + echo "stripping (with command $cmd and flags $stripFlags) in $paths"; + local striperr; + striperr="$(mktemp --tmpdir="$TMPDIR" 'striperr.XXXXXX')"; + find $paths -type f "${excludeFlags[@]}" -a '!' -path "$prefix/lib/debug/*" -printf '%D-%i,%p\0' | sort -t, -k1,1 -u -z | cut -d, -f2- -z | xargs -r -0 -n1 -P "$NIX_BUILD_CORES" -- $cmd $stripFlags 2> "$striperr" || exit_code=$?; + [[ "$exit_code" = 123 || -z "$exit_code" ]] || ( cat "$striperr" 1>&2 && exit 1 ); + rm "$striperr"; + find $paths -name '*.a' -type f -exec $ranlibCmd '{}' \; 2> /dev/null; + fi +} +stripHash () +{ + + local strippedName casematchOpt=0; + strippedName="$(basename -- "$1")"; + shopt -q nocasematch && casematchOpt=1; + shopt -u nocasematch; + if [[ "$strippedName" =~ ^[a-z0-9]{32}- ]]; then + echo "${strippedName:33}"; + else + echo "$strippedName"; + fi; + if (( casematchOpt )); then + shopt -s nocasematch; + fi +} +substitute () +{ + + local input="$1"; + local output="$2"; + shift 2; + if [ ! -f "$input" ]; then + echo "substitute(): ERROR: file '$input' does not exist" 1>&2; + return 1; + fi; + local content; + consumeEntire content < "$input"; + if [ -e "$output" ]; then + chmod +w "$output"; + fi; + substituteStream content "file '$input'" "$@" > "$output" +} +substituteAll () +{ + + local input="$1"; + local output="$2"; + local -a args=(); + _allFlags; + substitute "$input" "$output" "${args[@]}" +} +substituteAllInPlace () +{ + + local fileName="$1"; + shift; + substituteAll "$fileName" "$fileName" "$@" +} +substituteAllStream () +{ + + local -a args=(); + _allFlags; + substituteStream "$1" "$2" "${args[@]}" +} +substituteInPlace () +{ + + local -a fileNames=(); + for arg in "$@"; + do + if [[ "$arg" = "--"* ]]; then + break; + fi; + fileNames+=("$arg"); + shift; + done; + if ! [[ "${#fileNames[@]}" -gt 0 ]]; then + echo "substituteInPlace called without any files to operate on (files must come before options!)" 1>&2; + return 1; + fi; + for file in "${fileNames[@]}"; + do + substitute "$file" "$file" "$@"; + done +} +substituteStream () +{ + + local var=$1; + local description=$2; + shift 2; + while (( "$#" )); do + local replace_mode="$1"; + case "$1" in + --replace) + if ! "$_substituteStream_has_warned_replace_deprecation"; then + echo "substituteStream() in derivation $name: WARNING: '--replace' is deprecated, use --replace-{fail,warn,quiet}. ($description)" 1>&2; + _substituteStream_has_warned_replace_deprecation=true; + fi; + replace_mode='--replace-warn' + ;& + --replace-quiet | --replace-warn | --replace-fail) + pattern="$2"; + replacement="$3"; + shift 3; + if ! [[ "${!var}" == *"$pattern"* ]]; then + if [ "$replace_mode" == --replace-warn ]; then + printf "substituteStream() in derivation $name: WARNING: pattern %q doesn't match anything in %s\n" "$pattern" "$description" 1>&2; + else + if [ "$replace_mode" == --replace-fail ]; then + printf "substituteStream() in derivation $name: ERROR: pattern %q doesn't match anything in %s\n" "$pattern" "$description" 1>&2; + return 1; + fi; + fi; + fi; + eval "$var"'=${'"$var"'//"$pattern"/"$replacement"}' + ;; + --subst-var) + local varName="$2"; + shift 2; + if ! [[ "$varName" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then + echo "substituteStream() in derivation $name: ERROR: substitution variables must be valid Bash names, \"$varName\" isn't." 1>&2; + return 1; + fi; + if [ -z ${!varName+x} ]; then + echo "substituteStream() in derivation $name: ERROR: variable \$$varName is unset" 1>&2; + return 1; + fi; + pattern="@$varName@"; + replacement="${!varName}"; + eval "$var"'=${'"$var"'//"$pattern"/"$replacement"}' + ;; + --subst-var-by) + pattern="@$2@"; + replacement="$3"; + eval "$var"'=${'"$var"'//"$pattern"/"$replacement"}'; + shift 3 + ;; + *) + echo "substituteStream() in derivation $name: ERROR: Invalid command line argument: $1" 1>&2; + return 1 + ;; + esac; + done; + printf "%s" "${!var}" +} +toPythonPath () +{ + + local paths="$1"; + local result=; + for i in $paths; + do + p="$i/lib/python3.13/site-packages"; + result="${result}${result:+:}$p"; + done; + echo $result +} +unpackFile () +{ + + curSrc="$1"; + echo "unpacking source archive $curSrc"; + if ! runOneHook unpackCmd "$curSrc"; then + echo "do not know how to unpack source archive $curSrc"; + exit 1; + fi +} +unpackPhase () +{ + + runHook preUnpack; + if [ -z "${srcs:-}" ]; then + if [ -z "${src:-}" ]; then + echo 'variable $src or $srcs should point to the source'; + exit 1; + fi; + srcs="$src"; + fi; + local -a srcsArray; + concatTo srcsArray srcs; + local dirsBefore=""; + for i in *; + do + if [ -d "$i" ]; then + dirsBefore="$dirsBefore $i "; + fi; + done; + for i in "${srcsArray[@]}"; + do + unpackFile "$i"; + done; + : "${sourceRoot=}"; + if [ -n "${setSourceRoot:-}" ]; then + runOneHook setSourceRoot; + else + if [ -z "$sourceRoot" ]; then + for i in *; + do + if [ -d "$i" ]; then + case $dirsBefore in + *\ $i\ *) + + ;; + *) + if [ -n "$sourceRoot" ]; then + echo "unpacker produced multiple directories"; + exit 1; + fi; + sourceRoot="$i" + ;; + esac; + fi; + done; + fi; + fi; + if [ -z "$sourceRoot" ]; then + echo "unpacker appears to have produced no directories"; + exit 1; + fi; + echo "source root is $sourceRoot"; + if [ "${dontMakeSourcesWritable:-0}" != 1 ]; then + chmod -R u+w -- "$sourceRoot"; + fi; + runHook postUnpack +} +updateAutotoolsGnuConfigScriptsPhase () +{ + + if [ -n "${dontUpdateAutotoolsGnuConfigScripts-}" ]; then + return; + fi; + for script in config.sub config.guess; + do + for f in $(find . -type f -name "$script"); + do + echo "Updating Autotools / GNU config script to a newer upstream version: $f"; + cp -f "/nix/store/xb4f90wzr6nca7a1wy9ry9p2hvlhhsxx-gnu-config-2024-01-01/$script" "$f"; + done; + done +} +updateSourceDateEpoch () +{ + + local path="$1"; + [[ $path == -* ]] && path="./$path"; + local -a res=($(find "$path" -type f -not -newer "$NIX_BUILD_TOP/.." -printf '%T@ "%p"\0' | sort -n --zero-terminated | tail -n1 --zero-terminated | head -c -1)); + local time="${res[0]//\.[0-9]*/}"; + local newestFile="${res[1]}"; + if [ "${time:-0}" -gt "$SOURCE_DATE_EPOCH" ]; then + echo "setting SOURCE_DATE_EPOCH to timestamp $time of file $newestFile"; + export SOURCE_DATE_EPOCH="$time"; + local now="$(date +%s)"; + if [ "$time" -gt $((now - 60)) ]; then + echo "warning: file $newestFile may be generated; SOURCE_DATE_EPOCH may be non-deterministic"; + fi; + fi +} +PATH="$PATH${nix_saved_PATH:+:$nix_saved_PATH}" +XDG_DATA_DIRS="$XDG_DATA_DIRS${nix_saved_XDG_DATA_DIRS:+:$nix_saved_XDG_DATA_DIRS}" +export NIX_BUILD_TOP="$(mktemp -d -t nix-shell.XXXXXX)" +export TMP="$NIX_BUILD_TOP" +export TMPDIR="$NIX_BUILD_TOP" +export TEMP="$NIX_BUILD_TOP" +export TEMPDIR="$NIX_BUILD_TOP" +eval "${shellHook:-}" diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitlint b/.gitlint new file mode 100644 index 0000000..2a32b35 --- /dev/null +++ b/.gitlint @@ -0,0 +1,9 @@ +[general] +ignore=B6 + + +[title-max-length] +line-length=72 + +[title-match-regex] +regex=^(feat|fix|chore|docs|refactor|test|ci)(\(.+\))?: .+ \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 120000 index 0000000..7e5f932 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1 @@ +/nix/store/0qfkflnjv85f31nikijv5a5icjn5xa5m-pre-commit-config.json \ No newline at end of file diff --git a/MODULE.bazel b/MODULE.bazel index a270b92..68fed09 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -8,6 +8,7 @@ bazel_dep( repo_name = "io_bazel_rules_go", version = "0.60.0", ) +bazel_dep(name = "gazelle", version = "0.40.0") bazel_dep(name = "rules_bun", version = "0.2.3") bazel_dep(name = "rules_shell", version = "0.6.1") @@ -34,3 +35,13 @@ register_toolchains( "@rules_bun//bun:linux_x64_toolchain", "@rules_bun//bun:windows_x64_toolchain", ) + +go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps") +go_deps.from_file(go_mod = "//:go.mod") + +use_repo( + go_deps, + "bazel_gazelle_go_repository_config", + "com_github_wailsapp_wails_v3", + "org_golang_x_mod", +) diff --git a/docs/updates/bundle-archive-v1.md b/docs/updates/bundle-archive-v1.md new file mode 100644 index 0000000..5b80f0b --- /dev/null +++ b/docs/updates/bundle-archive-v1.md @@ -0,0 +1,32 @@ +# Bundle Archive v1 + +Downloaded updates are bundle archives rather than installer executables. + +Archive requirements: + +- top-level `bundle.json` +- payload files stored relative to the install root +- no absolute paths +- no `..` path traversal +- provider checksum must match the downloaded archive before staging + +`bundle.json`: + +```json +{ + "schemaVersion": 1, + "entrypoint": "MyApp.exe", + "files": [ + { "path": "MyApp.exe", "mode": "0755" }, + { "path": "resources/config.json", "mode": "0644" } + ] +} +``` + +Install roots: + +- macOS: the `.app` bundle directory +- Windows: the directory containing the running `.exe` +- Linux: the directory containing the running binary + +The helper-mode apply flow stages the archive, validates `bundle.json`, backs up existing files, copies payload files into the install root, and relaunches the app using `entrypoint`. diff --git a/docs/updates/http-manifest-v1.md b/docs/updates/http-manifest-v1.md new file mode 100644 index 0000000..ea4d0e1 --- /dev/null +++ b/docs/updates/http-manifest-v1.md @@ -0,0 +1,39 @@ +# HTTP Manifest v1 + +The generic update provider uses a first-party JSON manifest. + +Schema: + +```json +{ + "schemaVersion": 1, + "productID": "com.example.app", + "releases": [ + { + "id": "1.2.0", + "version": "1.2.0", + "channel": "stable", + "publishedAt": "2026-03-01T03:10:56Z", + "notesMarkdown": "Bug fixes", + "artifacts": [ + { + "os": "darwin", + "arch": "arm64", + "kind": "bundle-archive", + "format": "zip", + "url": "https://updates.example.com/app/1.2.0/darwin-arm64.zip", + "sha256": "..." + } + ] + } + ] +} +``` + +Rules: + +- `schemaVersion` must be `1` +- `productID` must match the running app descriptor +- `channel` is one of `stable`, `beta`, or `alpha` +- the provider picks the highest semver above the current version for the current `os` and `arch` +- artifact URLs are fetched with the adapter's per-provider request preparation hook diff --git a/examples/wails3_init_updater/.gitignore b/examples/wails3_init_updater/.gitignore new file mode 100644 index 0000000..ba8194a --- /dev/null +++ b/examples/wails3_init_updater/.gitignore @@ -0,0 +1,6 @@ +.task +bin +frontend/dist +frontend/node_modules +build/linux/appimage/build +build/windows/nsis/MicrosoftEdgeWebview2Setup.exe \ No newline at end of file diff --git a/examples/wails3_init_updater/README.md b/examples/wails3_init_updater/README.md new file mode 100644 index 0000000..adf9254 --- /dev/null +++ b/examples/wails3_init_updater/README.md @@ -0,0 +1,55 @@ +# wails3_init_updater + +This example started from the upstream scaffold generated on March 12, 2026 with: + +```bash +go run github.com/wailsapp/wails/v3/cmd/wails3@v3.0.0-alpha.74 init \ + -n updater-example \ + -t vanilla-ts \ + -d examples/wails3_init_updater \ + -q \ + -skipgomodtidy +``` + +It has been adapted to consume the local updater library from this repository. + +## What it demonstrates + +- a Wails 3 app binding the updater controller through a thin Wails service +- an authenticated HTTP manifest provider +- generated Wails bindings for updater methods +- a mock update server for local testing + +## Running the mock server + +```bash +go run ./cmd/mockupdateserver +``` + +The server listens on `http://127.0.0.1:18765` and expects: + +- `Authorization: Bearer test-token` + +## Running the app + +```bash +wails3 task dev +``` + +The app reads: + +- `UPDATER_MANIFEST_URL` +- `UPDATER_TOKEN` + +Defaults: + +- `UPDATER_MANIFEST_URL=http://127.0.0.1:18765/manifest.json` +- `UPDATER_TOKEN=test-token` + +## Backend verification + +```bash +go test ./... +``` + +The example test covers controller/service construction against the mock authenticated feed without launching a GUI. diff --git a/examples/wails3_init_updater/Taskfile.yml b/examples/wails3_init_updater/Taskfile.yml new file mode 100644 index 0000000..7f0550a --- /dev/null +++ b/examples/wails3_init_updater/Taskfile.yml @@ -0,0 +1,60 @@ +version: "3" + +includes: + common: ./build/Taskfile.yml + windows: ./build/windows/Taskfile.yml + darwin: ./build/darwin/Taskfile.yml + linux: ./build/linux/Taskfile.yml + ios: ./build/ios/Taskfile.yml + android: ./build/android/Taskfile.yml + +vars: + APP_NAME: "updater-example" + BIN_DIR: "bin" + VITE_PORT: "{{.WAILS_VITE_PORT | default 9245}}" + +tasks: + build: + summary: Builds the application + cmds: + - task: "{{OS}}:build" + + package: + summary: Packages a production build of the application + cmds: + - task: "{{OS}}:package" + + run: + summary: Runs the application + cmds: + - task: "{{OS}}:run" + + dev: + summary: Runs the application in development mode + cmds: + - wails3 dev -config ./build/config.yml -port {{.VITE_PORT}} + + setup:docker: + summary: Builds Docker image for cross-compilation (~800MB download) + cmds: + - task: common:setup:docker + + build:server: + summary: Builds the application in server mode (no GUI, HTTP server only) + cmds: + - task: common:build:server + + run:server: + summary: Runs the application in server mode + cmds: + - task: common:run:server + + build:docker: + summary: Builds a Docker image for server mode deployment + cmds: + - task: common:build:docker + + run:docker: + summary: Builds and runs the Docker image + cmds: + - task: common:run:docker diff --git a/examples/wails3_init_updater/build/Taskfile.yml b/examples/wails3_init_updater/build/Taskfile.yml new file mode 100644 index 0000000..1ed367a --- /dev/null +++ b/examples/wails3_init_updater/build/Taskfile.yml @@ -0,0 +1,251 @@ +version: "3" + +tasks: + go:mod:tidy: + summary: Runs `go mod tidy` + internal: true + cmds: + - go mod tidy + + install:frontend:deps: + summary: Install frontend dependencies + dir: frontend + sources: + - package.json + - package-lock.json + generates: + - node_modules + preconditions: + - sh: npm version + msg: "Looks like npm isn't installed. Npm is part of the Node installer: https://nodejs.org/en/download/" + cmds: + - npm install + + build:frontend: + label: build:frontend (DEV={{.DEV}}) + summary: Build the frontend project + dir: frontend + sources: + - "**/*" + - exclude: node_modules/**/* + generates: + - dist/**/* + deps: + - task: install:frontend:deps + - task: generate:bindings + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + cmds: + - npm run {{.BUILD_COMMAND}} -q + env: + PRODUCTION: '{{if eq .DEV "true"}}false{{else}}true{{end}}' + vars: + BUILD_COMMAND: '{{if eq .DEV "true"}}build:dev{{else}}build{{end}}' + + frontend:vendor:puppertino: + summary: Fetches Puppertino CSS into frontend/public for consistent mobile styling + sources: + - frontend/public/puppertino/puppertino.css + generates: + - frontend/public/puppertino/puppertino.css + cmds: + - | + set -euo pipefail + mkdir -p frontend/public/puppertino + # If bundled Puppertino exists, prefer it. Otherwise, try to fetch, but don't fail build on error. + if [ ! -f frontend/public/puppertino/puppertino.css ]; then + echo "No bundled Puppertino found. Attempting to fetch from GitHub..." + if curl -fsSL https://raw.githubusercontent.com/codedgar/Puppertino/main/dist/css/full.css -o frontend/public/puppertino/puppertino.css; then + curl -fsSL https://raw.githubusercontent.com/codedgar/Puppertino/main/LICENSE -o frontend/public/puppertino/LICENSE || true + echo "Puppertino CSS downloaded to frontend/public/puppertino/puppertino.css" + else + echo "Warning: Could not fetch Puppertino CSS. Proceeding without download since template may bundle it." + fi + else + echo "Using bundled Puppertino at frontend/public/puppertino/puppertino.css" + fi + # Ensure index.html includes Puppertino CSS and button classes + INDEX_HTML=frontend/index.html + if [ -f "$INDEX_HTML" ]; then + if ! grep -q 'href="/puppertino/puppertino.css"' "$INDEX_HTML"; then + # Insert Puppertino link tag after style.css link + awk ' + /href="\/style.css"\/?/ && !x { print; print " "; x=1; next }1 + ' "$INDEX_HTML" > "$INDEX_HTML.tmp" && mv "$INDEX_HTML.tmp" "$INDEX_HTML" + fi + # Replace default .btn with Puppertino primary button classes if present + sed -E -i'' 's/class=\"btn\"/class=\"p-btn p-prim-col\"/g' "$INDEX_HTML" || true + fi + + generate:bindings: + label: generate:bindings (BUILD_FLAGS={{.BUILD_FLAGS}}) + summary: Generates bindings for the frontend + deps: + - task: go:mod:tidy + sources: + - "**/*.[jt]s" + - exclude: frontend/**/* + - frontend/bindings/**/* # Rerun when switching between dev/production mode causes changes in output + - "**/*.go" + - go.mod + - go.sum + generates: + - frontend/bindings/**/* + cmds: + - wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true -ts + + generate:icons: + summary: Generates Windows `.ico` and Mac `.icns` from an image; on macOS, `-iconcomposerinput appicon.icon -macassetdir darwin` also produces `Assets.car` from a `.icon` file (skipped on other platforms). + dir: build + sources: + - "appicon.png" + - "appicon.icon" + generates: + - "darwin/icons.icns" + - "windows/icon.ico" + cmds: + - wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icon.ico -iconcomposerinput appicon.icon -macassetdir darwin + + dev:frontend: + summary: Runs the frontend in development mode + dir: frontend + deps: + - task: install:frontend:deps + cmds: + - npm run dev -- --port {{.VITE_PORT}} --strictPort + + update:build-assets: + summary: Updates the build assets + dir: build + cmds: + - wails3 update build-assets -name "{{.APP_NAME}}" -binaryname "{{.APP_NAME}}" -config config.yml -dir . + + build:server: + summary: Builds the application in server mode (no GUI, HTTP server only) + desc: | + Builds the application with the server build tag enabled. + Server mode runs as a pure HTTP server without native GUI dependencies. + Usage: task build:server + deps: + - task: build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + cmds: + - go build -tags server {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}-server{{exeExt}} + vars: + BUILD_FLAGS: "{{.BUILD_FLAGS}}" + + run:server: + summary: Builds and runs the application in server mode + deps: + - task: build:server + cmds: + - ./{{.BIN_DIR}}/{{.APP_NAME}}-server{{exeExt}} + + build:docker: + summary: Builds a Docker image for server mode deployment + desc: | + Creates a minimal Docker image containing the server mode binary. + The image is based on distroless for security and small size. + Usage: task build:docker [TAG=myapp:latest] + cmds: + - docker build -t {{.TAG | default (printf "%s:latest" .APP_NAME)}} -f build/docker/Dockerfile.server . + vars: + TAG: "{{.TAG}}" + preconditions: + - sh: docker info > /dev/null 2>&1 + msg: "Docker is required. Please install Docker first." + - sh: test -f build/docker/Dockerfile.server + msg: "Dockerfile.server not found. Run 'wails3 update build-assets' to generate it." + + run:docker: + summary: Builds and runs the Docker image + desc: | + Builds the Docker image and runs it, exposing port 8080. + Usage: task run:docker [TAG=myapp:latest] [PORT=8080] + Note: The internal container port is always 8080. The PORT variable + only changes the host port mapping. Ensure your app uses port 8080 + or modify the Dockerfile to match your ServerOptions.Port setting. + deps: + - task: build:docker + vars: + TAG: + ref: .TAG + cmds: + - docker run --rm -p {{.PORT | default "8080"}}:8080 {{.TAG | default (printf "%s:latest" .APP_NAME)}} + vars: + TAG: "{{.TAG}}" + PORT: "{{.PORT}}" + + setup:docker: + summary: Builds Docker image for cross-compilation (~800MB download) + desc: | + Builds the Docker image needed for cross-compiling to any platform. + Run this once to enable cross-platform builds from any OS. + cmds: + - docker build -t wails-cross -f build/docker/Dockerfile.cross build/docker/ + preconditions: + - sh: docker info > /dev/null 2>&1 + msg: "Docker is required. Please install Docker first." + + ios:device:list: + summary: Lists connected iOS devices (UDIDs) + cmds: + - xcrun xcdevice list + + ios:run:device: + summary: Build, install, and launch on a physical iPhone using Apple tools (xcodebuild/devicectl) + vars: + PROJECT: "{{.PROJECT}}" # e.g., build/ios/xcode/.xcodeproj + SCHEME: "{{.SCHEME}}" # e.g., ios.dev + CONFIG: '{{.CONFIG | default "Debug"}}' + DERIVED: '{{.DERIVED | default "build/ios/DerivedData"}}' + UDID: "{{.UDID}}" # from `task ios:device:list` + BUNDLE_ID: "{{.BUNDLE_ID}}" # e.g., com.yourco.wails.ios.dev + TEAM_ID: "{{.TEAM_ID}}" # optional, if your project is not already set up for signing + preconditions: + - sh: xcrun -f xcodebuild + msg: "xcodebuild not found. Please install Xcode." + - sh: xcrun -f devicectl + msg: "devicectl not found. Please update to Xcode 15+ (which includes devicectl)." + - sh: test -n '{{.PROJECT}}' + msg: "Set PROJECT to your .xcodeproj path (e.g., PROJECT=build/ios/xcode/App.xcodeproj)." + - sh: test -n '{{.SCHEME}}' + msg: "Set SCHEME to your app scheme (e.g., SCHEME=ios.dev)." + - sh: test -n '{{.UDID}}' + msg: "Set UDID to your device UDID (see: task ios:device:list)." + - sh: test -n '{{.BUNDLE_ID}}' + msg: "Set BUNDLE_ID to your app's bundle identifier (e.g., com.yourco.wails.ios.dev)." + cmds: + - | + set -euo pipefail + echo "Building for device: UDID={{.UDID}} SCHEME={{.SCHEME}} PROJECT={{.PROJECT}}" + XCB_ARGS=( + -project "{{.PROJECT}}" + -scheme "{{.SCHEME}}" + -configuration "{{.CONFIG}}" + -destination "id={{.UDID}}" + -derivedDataPath "{{.DERIVED}}" + -allowProvisioningUpdates + -allowProvisioningDeviceRegistration + ) + # Optionally inject signing identifiers if provided + if [ -n '{{.TEAM_ID}}' ]; then XCB_ARGS+=(DEVELOPMENT_TEAM={{.TEAM_ID}}); fi + if [ -n '{{.BUNDLE_ID}}' ]; then XCB_ARGS+=(PRODUCT_BUNDLE_IDENTIFIER={{.BUNDLE_ID}}); fi + xcodebuild "${XCB_ARGS[@]}" build | xcpretty || true + # If xcpretty isn't installed, run without it + if [ "${PIPESTATUS[0]}" -ne 0 ]; then + xcodebuild "${XCB_ARGS[@]}" build + fi + # Find built .app + APP_PATH=$(find "{{.DERIVED}}/Build/Products" -type d -name "*.app" -maxdepth 3 | head -n 1) + if [ -z "$APP_PATH" ]; then + echo "Could not locate built .app under {{.DERIVED}}/Build/Products" >&2 + exit 1 + fi + echo "Installing: $APP_PATH" + xcrun devicectl device install app --device "{{.UDID}}" "$APP_PATH" + echo "Launching: {{.BUNDLE_ID}}" + xcrun devicectl device process launch --device "{{.UDID}}" --stderr console --stdout console "{{.BUNDLE_ID}}" diff --git a/examples/wails3_init_updater/build/android/Taskfile.yml b/examples/wails3_init_updater/build/android/Taskfile.yml new file mode 100644 index 0000000..439a8a7 --- /dev/null +++ b/examples/wails3_init_updater/build/android/Taskfile.yml @@ -0,0 +1,237 @@ +version: "3" + +includes: + common: ../Taskfile.yml + +vars: + APP_ID: '{{.APP_ID | default "com.wails.app"}}' + MIN_SDK: "21" + TARGET_SDK: "34" + NDK_VERSION: "r26d" + +tasks: + install:deps: + summary: Check and install Android development dependencies + cmds: + - go run build/android/scripts/deps/install_deps.go + env: + TASK_FORCE_YES: "{{if .YES}}true{{else}}false{{end}}" + prompt: This will check and install Android development dependencies. Continue? + + build: + summary: Creates a build of the application for Android + deps: + - task: common:go:mod:tidy + - task: generate:android:bindings + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + PRODUCTION: + ref: .PRODUCTION + - task: common:generate:icons + cmds: + - echo "Building Android app {{.APP_NAME}}..." + - task: compile:go:shared + vars: + ARCH: '{{.ARCH | default "arm64"}}' + vars: + BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production,android -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-tags android,debug -buildvcs=false -gcflags=all="-l"{{end}}' + env: + PRODUCTION: '{{.PRODUCTION | default "false"}}' + + compile:go:shared: + summary: Compile Go code to shared library (.so) + cmds: + - | + NDK_ROOT="${ANDROID_NDK_HOME:-$ANDROID_HOME/ndk/{{.NDK_VERSION}}}" + if [ ! -d "$NDK_ROOT" ]; then + echo "Error: Android NDK not found at $NDK_ROOT" + echo "Please set ANDROID_NDK_HOME or install NDK {{.NDK_VERSION}} via Android Studio" + exit 1 + fi + + # Determine toolchain based on host OS + case "$(uname -s)" in + Darwin) HOST_TAG="darwin-x86_64" ;; + Linux) HOST_TAG="linux-x86_64" ;; + *) echo "Unsupported host OS"; exit 1 ;; + esac + + TOOLCHAIN="$NDK_ROOT/toolchains/llvm/prebuilt/$HOST_TAG" + + # Set compiler based on architecture + case "{{.ARCH}}" in + arm64) + export CC="$TOOLCHAIN/bin/aarch64-linux-android{{.MIN_SDK}}-clang" + export CXX="$TOOLCHAIN/bin/aarch64-linux-android{{.MIN_SDK}}-clang++" + export GOARCH=arm64 + JNI_DIR="arm64-v8a" + ;; + amd64|x86_64) + export CC="$TOOLCHAIN/bin/x86_64-linux-android{{.MIN_SDK}}-clang" + export CXX="$TOOLCHAIN/bin/x86_64-linux-android{{.MIN_SDK}}-clang++" + export GOARCH=amd64 + JNI_DIR="x86_64" + ;; + *) + echo "Unsupported architecture: {{.ARCH}}" + exit 1 + ;; + esac + + export CGO_ENABLED=1 + export GOOS=android + + mkdir -p {{.BIN_DIR}} + mkdir -p build/android/app/src/main/jniLibs/$JNI_DIR + + go build -buildmode=c-shared {{.BUILD_FLAGS}} \ + -o build/android/app/src/main/jniLibs/$JNI_DIR/libwails.so + vars: + BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production,android -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-tags android,debug -buildvcs=false -gcflags=all="-l"{{end}}' + + compile:go:all-archs: + summary: Compile Go code for all Android architectures (fat APK) + cmds: + - task: compile:go:shared + vars: + ARCH: arm64 + - task: compile:go:shared + vars: + ARCH: amd64 + + package: + summary: Packages a production build of the application into an APK + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: assemble:apk + + package:fat: + summary: Packages a production build for all architectures (fat APK) + cmds: + - task: compile:go:all-archs + - task: assemble:apk + + assemble:apk: + summary: Assembles the APK using Gradle + cmds: + - | + cd build/android + ./gradlew assembleDebug + cp app/build/outputs/apk/debug/app-debug.apk "../../{{.BIN_DIR}}/{{.APP_NAME}}.apk" + echo "APK created: {{.BIN_DIR}}/{{.APP_NAME}}.apk" + + assemble:apk:release: + summary: Assembles a release APK using Gradle + cmds: + - | + cd build/android + ./gradlew assembleRelease + cp app/build/outputs/apk/release/app-release-unsigned.apk "../../{{.BIN_DIR}}/{{.APP_NAME}}-release.apk" + echo "Release APK created: {{.BIN_DIR}}/{{.APP_NAME}}-release.apk" + + generate:android:bindings: + internal: true + summary: Generates bindings for Android + sources: + - "**/*.go" + - go.mod + - go.sum + generates: + - frontend/bindings/**/* + cmds: + - wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true + env: + GOOS: android + CGO_ENABLED: 1 + GOARCH: '{{.ARCH | default "arm64"}}' + + ensure-emulator: + internal: true + summary: Ensure Android Emulator is running + silent: true + cmds: + - | + # Check if an emulator is already running + if adb devices | grep -q "emulator"; then + echo "Emulator already running" + exit 0 + fi + + # Get first available AVD + AVD_NAME=$(emulator -list-avds | head -1) + if [ -z "$AVD_NAME" ]; then + echo "No Android Virtual Devices found." + echo "Create one using: Android Studio > Tools > Device Manager" + exit 1 + fi + + echo "Starting emulator: $AVD_NAME" + emulator -avd "$AVD_NAME" -no-snapshot-load & + + # Wait for emulator to boot (max 60 seconds) + echo "Waiting for emulator to boot..." + adb wait-for-device + + for i in {1..60}; do + BOOT_COMPLETED=$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r') + if [ "$BOOT_COMPLETED" = "1" ]; then + echo "Emulator booted successfully" + exit 0 + fi + sleep 1 + done + + echo "Emulator boot timeout" + exit 1 + preconditions: + - sh: command -v adb + msg: "adb not found. Please install Android SDK and add platform-tools to PATH" + - sh: command -v emulator + msg: "emulator not found. Please install Android SDK and add emulator to PATH" + + deploy-emulator: + summary: Deploy to Android Emulator + deps: [package] + cmds: + - adb uninstall {{.APP_ID}} 2>/dev/null || true + - adb install "{{.BIN_DIR}}/{{.APP_NAME}}.apk" + - adb shell am start -n {{.APP_ID}}/.MainActivity + + run: + summary: Run the application in Android Emulator + deps: + - task: ensure-emulator + - task: build + vars: + ARCH: x86_64 + cmds: + - task: assemble:apk + - adb uninstall {{.APP_ID}} 2>/dev/null || true + - adb install "{{.BIN_DIR}}/{{.APP_NAME}}.apk" + - adb shell am start -n {{.APP_ID}}/.MainActivity + + logs: + summary: Stream Android logcat filtered to this app + cmds: + - adb logcat -v time | grep -E "(Wails|{{.APP_NAME}})" + + logs:all: + summary: Stream all Android logcat (verbose) + cmds: + - adb logcat -v time + + clean: + summary: Clean build artifacts + cmds: + - rm -rf {{.BIN_DIR}} + - rm -rf build/android/app/build + - rm -rf build/android/app/src/main/jniLibs/*/libwails.so + - rm -rf build/android/.gradle diff --git a/examples/wails3_init_updater/build/android/app/build.gradle b/examples/wails3_init_updater/build/android/app/build.gradle new file mode 100644 index 0000000..78fdbf7 --- /dev/null +++ b/examples/wails3_init_updater/build/android/app/build.gradle @@ -0,0 +1,63 @@ +plugins { + id 'com.android.application' +} + +android { + namespace 'com.wails.app' + compileSdk 34 + + buildFeatures { + buildConfig = true + } + + defaultConfig { + applicationId "com.wails.app" + minSdk 21 + targetSdk 34 + versionCode 1 + versionName "1.0" + + // Configure supported ABIs + ndk { + abiFilters 'arm64-v8a', 'x86_64' + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + debug { + debuggable true + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + + // Source sets configuration + sourceSets { + main { + // JNI libraries are in jniLibs folder + jniLibs.srcDirs = ['src/main/jniLibs'] + // Assets for the WebView + assets.srcDirs = ['src/main/assets'] + } + } + + // Packaging options + packagingOptions { + // Don't strip Go symbols in debug builds + doNotStrip '*/arm64-v8a/libwails.so' + doNotStrip '*/x86_64/libwails.so' + } +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.webkit:webkit:1.9.0' + implementation 'com.google.android.material:material:1.11.0' +} diff --git a/examples/wails3_init_updater/build/android/app/proguard-rules.pro b/examples/wails3_init_updater/build/android/app/proguard-rules.pro new file mode 100644 index 0000000..8b88c3d --- /dev/null +++ b/examples/wails3_init_updater/build/android/app/proguard-rules.pro @@ -0,0 +1,12 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. + +# Keep native methods +-keepclasseswithmembernames class * { + native ; +} + +# Keep Wails bridge classes +-keep class com.wails.app.WailsBridge { *; } +-keep class com.wails.app.WailsJSBridge { *; } diff --git a/examples/wails3_init_updater/build/android/app/src/main/AndroidManifest.xml b/examples/wails3_init_updater/build/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6c7982a --- /dev/null +++ b/examples/wails3_init_updater/build/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + diff --git a/examples/wails3_init_updater/build/android/app/src/main/java/com/wails/app/MainActivity.java b/examples/wails3_init_updater/build/android/app/src/main/java/com/wails/app/MainActivity.java new file mode 100644 index 0000000..3067fee --- /dev/null +++ b/examples/wails3_init_updater/build/android/app/src/main/java/com/wails/app/MainActivity.java @@ -0,0 +1,198 @@ +package com.wails.app; + +import android.annotation.SuppressLint; +import android.os.Bundle; +import android.util.Log; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.webkit.WebViewAssetLoader; +import com.wails.app.BuildConfig; + +/** + * MainActivity hosts the WebView and manages the Wails application lifecycle. + * It uses WebViewAssetLoader to serve assets from the Go library without + * requiring a network server. + */ +public class MainActivity extends AppCompatActivity { + private static final String TAG = "WailsActivity"; + private static final String WAILS_SCHEME = "https"; + private static final String WAILS_HOST = "wails.localhost"; + + private WebView webView; + private WailsBridge bridge; + private WebViewAssetLoader assetLoader; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + // Initialize the native Go library + bridge = new WailsBridge(this); + bridge.initialize(); + + // Set up WebView + setupWebView(); + + // Load the application + loadApplication(); + } + + @SuppressLint("SetJavaScriptEnabled") + private void setupWebView() { + webView = findViewById(R.id.webview); + + // Configure WebView settings + WebSettings settings = webView.getSettings(); + settings.setJavaScriptEnabled(true); + settings.setDomStorageEnabled(true); + settings.setDatabaseEnabled(true); + settings.setAllowFileAccess(false); + settings.setAllowContentAccess(false); + settings.setMediaPlaybackRequiresUserGesture(false); + settings.setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW); + + // Enable debugging in debug builds + if (BuildConfig.DEBUG) { + WebView.setWebContentsDebuggingEnabled(true); + } + + // Set up asset loader for serving local assets + assetLoader = new WebViewAssetLoader.Builder() + .setDomain(WAILS_HOST) + .addPathHandler("/", new WailsPathHandler(bridge)) + .build(); + + // Set up WebView client to intercept requests + webView.setWebViewClient(new WebViewClient() { + @Nullable + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { + String url = request.getUrl().toString(); + Log.d(TAG, "Intercepting request: " + url); + + // Handle wails.localhost requests + if (request.getUrl().getHost() != null && + request.getUrl().getHost().equals(WAILS_HOST)) { + + // For wails API calls (runtime, capabilities, etc.), we need to pass the full URL + // including query string because WebViewAssetLoader.PathHandler strips query params + String path = request.getUrl().getPath(); + if (path != null && path.startsWith("/wails/")) { + // Get full path with query string for runtime calls + String fullPath = path; + String query = request.getUrl().getQuery(); + if (query != null && !query.isEmpty()) { + fullPath = path + "?" + query; + } + Log.d(TAG, "Wails API call detected, full path: " + fullPath); + + // Call bridge directly with full path + byte[] data = bridge.serveAsset(fullPath, request.getMethod(), "{}"); + if (data != null && data.length > 0) { + java.io.InputStream inputStream = new java.io.ByteArrayInputStream(data); + java.util.Map headers = new java.util.HashMap<>(); + headers.put("Access-Control-Allow-Origin", "*"); + headers.put("Cache-Control", "no-cache"); + headers.put("Content-Type", "application/json"); + + return new WebResourceResponse( + "application/json", + "UTF-8", + 200, + "OK", + headers, + inputStream + ); + } + // Return error response if data is null + return new WebResourceResponse( + "application/json", + "UTF-8", + 500, + "Internal Error", + new java.util.HashMap<>(), + new java.io.ByteArrayInputStream("{}".getBytes()) + ); + } + + // For regular assets, use the asset loader + return assetLoader.shouldInterceptRequest(request.getUrl()); + } + + return super.shouldInterceptRequest(view, request); + } + + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + Log.d(TAG, "Page loaded: " + url); + // Inject Wails runtime + bridge.injectRuntime(webView, url); + } + }); + + // Add JavaScript interface for Go communication + webView.addJavascriptInterface(new WailsJSBridge(bridge, webView), "wails"); + } + + private void loadApplication() { + // Load the main page from the asset server + String url = WAILS_SCHEME + "://" + WAILS_HOST + "/"; + Log.d(TAG, "Loading URL: " + url); + webView.loadUrl(url); + } + + /** + * Execute JavaScript in the WebView from the Go side + */ + public void executeJavaScript(final String js) { + runOnUiThread(() -> { + if (webView != null) { + webView.evaluateJavascript(js, null); + } + }); + } + + @Override + protected void onResume() { + super.onResume(); + if (bridge != null) { + bridge.onResume(); + } + } + + @Override + protected void onPause() { + super.onPause(); + if (bridge != null) { + bridge.onPause(); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (bridge != null) { + bridge.shutdown(); + } + if (webView != null) { + webView.destroy(); + } + } + + @Override + public void onBackPressed() { + if (webView != null && webView.canGoBack()) { + webView.goBack(); + } else { + super.onBackPressed(); + } + } +} diff --git a/examples/wails3_init_updater/build/android/app/src/main/java/com/wails/app/WailsBridge.java b/examples/wails3_init_updater/build/android/app/src/main/java/com/wails/app/WailsBridge.java new file mode 100644 index 0000000..3dab652 --- /dev/null +++ b/examples/wails3_init_updater/build/android/app/src/main/java/com/wails/app/WailsBridge.java @@ -0,0 +1,214 @@ +package com.wails.app; + +import android.content.Context; +import android.util.Log; +import android.webkit.WebView; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * WailsBridge manages the connection between the Java/Android side and the Go native library. + * It handles: + * - Loading and initializing the native Go library + * - Serving asset requests from Go + * - Passing messages between JavaScript and Go + * - Managing callbacks for async operations + */ +public class WailsBridge { + private static final String TAG = "WailsBridge"; + + static { + // Load the native Go library + System.loadLibrary("wails"); + } + + private final Context context; + private final AtomicInteger callbackIdGenerator = new AtomicInteger(0); + private final ConcurrentHashMap pendingAssetCallbacks = new ConcurrentHashMap<>(); + private final ConcurrentHashMap pendingMessageCallbacks = new ConcurrentHashMap<>(); + private WebView webView; + private volatile boolean initialized = false; + + // Native methods - implemented in Go + private static native void nativeInit(WailsBridge bridge); + private static native void nativeShutdown(); + private static native void nativeOnResume(); + private static native void nativeOnPause(); + private static native void nativeOnPageFinished(String url); + private static native byte[] nativeServeAsset(String path, String method, String headers); + private static native String nativeHandleMessage(String message); + private static native String nativeGetAssetMimeType(String path); + + public WailsBridge(Context context) { + this.context = context; + } + + /** + * Initialize the native Go library + */ + public void initialize() { + if (initialized) { + return; + } + + Log.i(TAG, "Initializing Wails bridge..."); + try { + nativeInit(this); + initialized = true; + Log.i(TAG, "Wails bridge initialized successfully"); + } catch (Exception e) { + Log.e(TAG, "Failed to initialize Wails bridge", e); + } + } + + /** + * Shutdown the native Go library + */ + public void shutdown() { + if (!initialized) { + return; + } + + Log.i(TAG, "Shutting down Wails bridge..."); + try { + nativeShutdown(); + initialized = false; + } catch (Exception e) { + Log.e(TAG, "Error during shutdown", e); + } + } + + /** + * Called when the activity resumes + */ + public void onResume() { + if (initialized) { + nativeOnResume(); + } + } + + /** + * Called when the activity pauses + */ + public void onPause() { + if (initialized) { + nativeOnPause(); + } + } + + /** + * Serve an asset from the Go asset server + * @param path The URL path requested + * @param method The HTTP method + * @param headers The request headers as JSON + * @return The asset data, or null if not found + */ + public byte[] serveAsset(String path, String method, String headers) { + if (!initialized) { + Log.w(TAG, "Bridge not initialized, cannot serve asset: " + path); + return null; + } + + Log.d(TAG, "Serving asset: " + path); + try { + return nativeServeAsset(path, method, headers); + } catch (Exception e) { + Log.e(TAG, "Error serving asset: " + path, e); + return null; + } + } + + /** + * Get the MIME type for an asset + * @param path The asset path + * @return The MIME type string + */ + public String getAssetMimeType(String path) { + if (!initialized) { + return "application/octet-stream"; + } + + try { + String mimeType = nativeGetAssetMimeType(path); + return mimeType != null ? mimeType : "application/octet-stream"; + } catch (Exception e) { + Log.e(TAG, "Error getting MIME type for: " + path, e); + return "application/octet-stream"; + } + } + + /** + * Handle a message from JavaScript + * @param message The message from JavaScript (JSON) + * @return The response to send back to JavaScript (JSON) + */ + public String handleMessage(String message) { + if (!initialized) { + Log.w(TAG, "Bridge not initialized, cannot handle message"); + return "{\"error\":\"Bridge not initialized\"}"; + } + + Log.d(TAG, "Handling message from JS: " + message); + try { + return nativeHandleMessage(message); + } catch (Exception e) { + Log.e(TAG, "Error handling message", e); + return "{\"error\":\"" + e.getMessage() + "\"}"; + } + } + + /** + * Inject the Wails runtime JavaScript into the WebView. + * Called when the page finishes loading. + * @param webView The WebView to inject into + * @param url The URL that finished loading + */ + public void injectRuntime(WebView webView, String url) { + this.webView = webView; + // Notify Go side that page has finished loading so it can inject the runtime + Log.d(TAG, "Page finished loading: " + url + ", notifying Go side"); + if (initialized) { + nativeOnPageFinished(url); + } + } + + /** + * Execute JavaScript in the WebView (called from Go side) + * @param js The JavaScript code to execute + */ + public void executeJavaScript(String js) { + if (webView != null) { + webView.post(() -> webView.evaluateJavascript(js, null)); + } + } + + /** + * Called from Go when an event needs to be emitted to JavaScript + * @param eventName The event name + * @param eventData The event data (JSON) + */ + public void emitEvent(String eventName, String eventData) { + String js = String.format("window.wails && window.wails._emit('%s', %s);", + escapeJsString(eventName), eventData); + executeJavaScript(js); + } + + private String escapeJsString(String str) { + return str.replace("\\", "\\\\") + .replace("'", "\\'") + .replace("\n", "\\n") + .replace("\r", "\\r"); + } + + // Callback interfaces + public interface AssetCallback { + void onAssetReady(byte[] data, String mimeType); + void onAssetError(String error); + } + + public interface MessageCallback { + void onResponse(String response); + void onError(String error); + } +} diff --git a/examples/wails3_init_updater/build/android/app/src/main/java/com/wails/app/WailsJSBridge.java b/examples/wails3_init_updater/build/android/app/src/main/java/com/wails/app/WailsJSBridge.java new file mode 100644 index 0000000..98ae5b2 --- /dev/null +++ b/examples/wails3_init_updater/build/android/app/src/main/java/com/wails/app/WailsJSBridge.java @@ -0,0 +1,142 @@ +package com.wails.app; + +import android.util.Log; +import android.webkit.JavascriptInterface; +import android.webkit.WebView; +import com.wails.app.BuildConfig; + +/** + * WailsJSBridge provides the JavaScript interface that allows the web frontend + * to communicate with the Go backend. This is exposed to JavaScript as the + * `window.wails` object. + * + * Similar to iOS's WKScriptMessageHandler but using Android's addJavascriptInterface. + */ +public class WailsJSBridge { + private static final String TAG = "WailsJSBridge"; + + private final WailsBridge bridge; + private final WebView webView; + + public WailsJSBridge(WailsBridge bridge, WebView webView) { + this.bridge = bridge; + this.webView = webView; + } + + /** + * Send a message to Go and return the response synchronously. + * Called from JavaScript: wails.invoke(message) + * + * @param message The message to send (JSON string) + * @return The response from Go (JSON string) + */ + @JavascriptInterface + public String invoke(String message) { + Log.d(TAG, "Invoke called: " + message); + return bridge.handleMessage(message); + } + + /** + * Send a message to Go asynchronously. + * The response will be sent back via a callback. + * Called from JavaScript: wails.invokeAsync(callbackId, message) + * + * @param callbackId The callback ID to use for the response + * @param message The message to send (JSON string) + */ + @JavascriptInterface + public void invokeAsync(final String callbackId, final String message) { + Log.d(TAG, "InvokeAsync called: " + message); + + // Handle in background thread to not block JavaScript + new Thread(() -> { + try { + String response = bridge.handleMessage(message); + sendCallback(callbackId, response, null); + } catch (Exception e) { + Log.e(TAG, "Error in async invoke", e); + sendCallback(callbackId, null, e.getMessage()); + } + }).start(); + } + + /** + * Log a message from JavaScript to Android's logcat + * Called from JavaScript: wails.log(level, message) + * + * @param level The log level (debug, info, warn, error) + * @param message The message to log + */ + @JavascriptInterface + public void log(String level, String message) { + switch (level.toLowerCase()) { + case "debug": + Log.d(TAG + "/JS", message); + break; + case "info": + Log.i(TAG + "/JS", message); + break; + case "warn": + Log.w(TAG + "/JS", message); + break; + case "error": + Log.e(TAG + "/JS", message); + break; + default: + Log.v(TAG + "/JS", message); + break; + } + } + + /** + * Get the platform name + * Called from JavaScript: wails.platform() + * + * @return "android" + */ + @JavascriptInterface + public String platform() { + return "android"; + } + + /** + * Check if we're running in debug mode + * Called from JavaScript: wails.isDebug() + * + * @return true if debug build, false otherwise + */ + @JavascriptInterface + public boolean isDebug() { + return BuildConfig.DEBUG; + } + + /** + * Send a callback response to JavaScript + */ + private void sendCallback(String callbackId, String result, String error) { + final String js; + if (error != null) { + js = String.format( + "window.wails && window.wails._callback('%s', null, '%s');", + escapeJsString(callbackId), + escapeJsString(error) + ); + } else { + js = String.format( + "window.wails && window.wails._callback('%s', %s, null);", + escapeJsString(callbackId), + result != null ? result : "null" + ); + } + + webView.post(() -> webView.evaluateJavascript(js, null)); + } + + private String escapeJsString(String str) { + if (str == null) return ""; + return str.replace("\\", "\\\\") + .replace("'", "\\'") + .replace("\n", "\\n") + .replace("\r", "\\r"); + } +} diff --git a/examples/wails3_init_updater/build/android/app/src/main/java/com/wails/app/WailsPathHandler.java b/examples/wails3_init_updater/build/android/app/src/main/java/com/wails/app/WailsPathHandler.java new file mode 100644 index 0000000..326fa9b --- /dev/null +++ b/examples/wails3_init_updater/build/android/app/src/main/java/com/wails/app/WailsPathHandler.java @@ -0,0 +1,118 @@ +package com.wails.app; + +import android.net.Uri; +import android.util.Log; +import android.webkit.WebResourceResponse; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.WebViewAssetLoader; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +/** + * WailsPathHandler implements WebViewAssetLoader.PathHandler to serve assets + * from the Go asset server. This allows the WebView to load assets without + * using a network server, similar to iOS's WKURLSchemeHandler. + */ +public class WailsPathHandler implements WebViewAssetLoader.PathHandler { + private static final String TAG = "WailsPathHandler"; + + private final WailsBridge bridge; + + public WailsPathHandler(WailsBridge bridge) { + this.bridge = bridge; + } + + @Nullable + @Override + public WebResourceResponse handle(@NonNull String path) { + Log.d(TAG, "Handling path: " + path); + + // Normalize path + if (path.isEmpty() || path.equals("/")) { + path = "/index.html"; + } + + // Get asset from Go + byte[] data = bridge.serveAsset(path, "GET", "{}"); + + if (data == null || data.length == 0) { + Log.w(TAG, "Asset not found: " + path); + return null; // Return null to let WebView handle 404 + } + + // Determine MIME type + String mimeType = bridge.getAssetMimeType(path); + Log.d(TAG, "Serving " + path + " with type " + mimeType + " (" + data.length + " bytes)"); + + // Create response + InputStream inputStream = new ByteArrayInputStream(data); + Map headers = new HashMap<>(); + headers.put("Access-Control-Allow-Origin", "*"); + headers.put("Cache-Control", "no-cache"); + + return new WebResourceResponse( + mimeType, + "UTF-8", + 200, + "OK", + headers, + inputStream + ); + } + + /** + * Determine MIME type from file extension + */ + private String getMimeType(String path) { + String lowerPath = path.toLowerCase(); + + if (lowerPath.endsWith(".html") || lowerPath.endsWith(".htm")) { + return "text/html"; + } else if (lowerPath.endsWith(".js") || lowerPath.endsWith(".mjs")) { + return "application/javascript"; + } else if (lowerPath.endsWith(".css")) { + return "text/css"; + } else if (lowerPath.endsWith(".json")) { + return "application/json"; + } else if (lowerPath.endsWith(".png")) { + return "image/png"; + } else if (lowerPath.endsWith(".jpg") || lowerPath.endsWith(".jpeg")) { + return "image/jpeg"; + } else if (lowerPath.endsWith(".gif")) { + return "image/gif"; + } else if (lowerPath.endsWith(".svg")) { + return "image/svg+xml"; + } else if (lowerPath.endsWith(".ico")) { + return "image/x-icon"; + } else if (lowerPath.endsWith(".woff")) { + return "font/woff"; + } else if (lowerPath.endsWith(".woff2")) { + return "font/woff2"; + } else if (lowerPath.endsWith(".ttf")) { + return "font/ttf"; + } else if (lowerPath.endsWith(".eot")) { + return "application/vnd.ms-fontobject"; + } else if (lowerPath.endsWith(".xml")) { + return "application/xml"; + } else if (lowerPath.endsWith(".txt")) { + return "text/plain"; + } else if (lowerPath.endsWith(".wasm")) { + return "application/wasm"; + } else if (lowerPath.endsWith(".mp3")) { + return "audio/mpeg"; + } else if (lowerPath.endsWith(".mp4")) { + return "video/mp4"; + } else if (lowerPath.endsWith(".webm")) { + return "video/webm"; + } else if (lowerPath.endsWith(".webp")) { + return "image/webp"; + } + + return "application/octet-stream"; + } +} diff --git a/examples/wails3_init_updater/build/android/app/src/main/res/layout/activity_main.xml b/examples/wails3_init_updater/build/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..f278384 --- /dev/null +++ b/examples/wails3_init_updater/build/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/examples/wails3_init_updater/build/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/examples/wails3_init_updater/build/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..9409abe Binary files /dev/null and b/examples/wails3_init_updater/build/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/examples/wails3_init_updater/build/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/examples/wails3_init_updater/build/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..9409abe Binary files /dev/null and b/examples/wails3_init_updater/build/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/examples/wails3_init_updater/build/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/examples/wails3_init_updater/build/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..5b6acc0 Binary files /dev/null and b/examples/wails3_init_updater/build/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/examples/wails3_init_updater/build/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/examples/wails3_init_updater/build/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..5b6acc0 Binary files /dev/null and b/examples/wails3_init_updater/build/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/examples/wails3_init_updater/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/examples/wails3_init_updater/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..1c2c664 Binary files /dev/null and b/examples/wails3_init_updater/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/examples/wails3_init_updater/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/examples/wails3_init_updater/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..1c2c664 Binary files /dev/null and b/examples/wails3_init_updater/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/examples/wails3_init_updater/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/examples/wails3_init_updater/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..be557d8 Binary files /dev/null and b/examples/wails3_init_updater/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/examples/wails3_init_updater/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/examples/wails3_init_updater/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..be557d8 Binary files /dev/null and b/examples/wails3_init_updater/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/examples/wails3_init_updater/build/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/examples/wails3_init_updater/build/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4507f32 Binary files /dev/null and b/examples/wails3_init_updater/build/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/examples/wails3_init_updater/build/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/examples/wails3_init_updater/build/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..4507f32 Binary files /dev/null and b/examples/wails3_init_updater/build/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/examples/wails3_init_updater/build/android/app/src/main/res/values/colors.xml b/examples/wails3_init_updater/build/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..dd33f3b --- /dev/null +++ b/examples/wails3_init_updater/build/android/app/src/main/res/values/colors.xml @@ -0,0 +1,8 @@ + + + #3574D4 + #2C5FB8 + #1B2636 + #FFFFFFFF + #FF000000 + diff --git a/examples/wails3_init_updater/build/android/app/src/main/res/values/strings.xml b/examples/wails3_init_updater/build/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..3ed9e47 --- /dev/null +++ b/examples/wails3_init_updater/build/android/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Wails App + diff --git a/examples/wails3_init_updater/build/android/app/src/main/res/values/themes.xml b/examples/wails3_init_updater/build/android/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..be8a282 --- /dev/null +++ b/examples/wails3_init_updater/build/android/app/src/main/res/values/themes.xml @@ -0,0 +1,14 @@ + + + + diff --git a/examples/wails3_init_updater/build/android/build.gradle b/examples/wails3_init_updater/build/android/build.gradle new file mode 100644 index 0000000..d7fbab3 --- /dev/null +++ b/examples/wails3_init_updater/build/android/build.gradle @@ -0,0 +1,4 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + id 'com.android.application' version '8.7.3' apply false +} diff --git a/examples/wails3_init_updater/build/android/gradle.properties b/examples/wails3_init_updater/build/android/gradle.properties new file mode 100644 index 0000000..b9d4426 --- /dev/null +++ b/examples/wails3_init_updater/build/android/gradle.properties @@ -0,0 +1,26 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. For more details, visit +# https://developer.android.com/build/optimize-your-build#parallel +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true + +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true diff --git a/examples/wails3_init_updater/build/android/gradle/wrapper/gradle-wrapper.jar b/examples/wails3_init_updater/build/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f8e1ee3 Binary files /dev/null and b/examples/wails3_init_updater/build/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/wails3_init_updater/build/android/gradle/wrapper/gradle-wrapper.properties b/examples/wails3_init_updater/build/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..23449a2 --- /dev/null +++ b/examples/wails3_init_updater/build/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/wails3_init_updater/build/android/gradlew b/examples/wails3_init_updater/build/android/gradlew new file mode 100644 index 0000000..adff685 --- /dev/null +++ b/examples/wails3_init_updater/build/android/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/examples/wails3_init_updater/build/android/gradlew.bat b/examples/wails3_init_updater/build/android/gradlew.bat new file mode 100644 index 0000000..e509b2d --- /dev/null +++ b/examples/wails3_init_updater/build/android/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/wails3_init_updater/build/android/main_android.go b/examples/wails3_init_updater/build/android/main_android.go new file mode 100644 index 0000000..70a7164 --- /dev/null +++ b/examples/wails3_init_updater/build/android/main_android.go @@ -0,0 +1,11 @@ +//go:build android + +package main + +import "github.com/wailsapp/wails/v3/pkg/application" + +func init() { + // Register main function to be called when the Android app initializes + // This is necessary because in c-shared build mode, main() is not automatically called + application.RegisterAndroidMain(main) +} diff --git a/examples/wails3_init_updater/build/android/scripts/deps/install_deps.go b/examples/wails3_init_updater/build/android/scripts/deps/install_deps.go new file mode 100644 index 0000000..d9dfedf --- /dev/null +++ b/examples/wails3_init_updater/build/android/scripts/deps/install_deps.go @@ -0,0 +1,151 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" +) + +func main() { + fmt.Println("Checking Android development dependencies...") + fmt.Println() + + errors := []string{} + + // Check Go + if !checkCommand("go", "version") { + errors = append(errors, "Go is not installed. Install from https://go.dev/dl/") + } else { + fmt.Println("✓ Go is installed") + } + + // Check ANDROID_HOME + androidHome := os.Getenv("ANDROID_HOME") + if androidHome == "" { + androidHome = os.Getenv("ANDROID_SDK_ROOT") + } + if androidHome == "" { + // Try common default locations + home, _ := os.UserHomeDir() + possiblePaths := []string{ + filepath.Join(home, "Android", "Sdk"), + filepath.Join(home, "Library", "Android", "sdk"), + "/usr/local/share/android-sdk", + } + for _, p := range possiblePaths { + if _, err := os.Stat(p); err == nil { + androidHome = p + break + } + } + } + + if androidHome == "" { + errors = append(errors, "ANDROID_HOME not set. Install Android Studio and set ANDROID_HOME environment variable") + } else { + fmt.Printf("✓ ANDROID_HOME: %s\n", androidHome) + } + + // Check adb + if !checkCommand("adb", "version") { + if androidHome != "" { + platformTools := filepath.Join(androidHome, "platform-tools") + errors = append(errors, fmt.Sprintf("adb not found. Add %s to PATH", platformTools)) + } else { + errors = append(errors, "adb not found. Install Android SDK Platform-Tools") + } + } else { + fmt.Println("✓ adb is installed") + } + + // Check emulator + if !checkCommand("emulator", "-list-avds") { + if androidHome != "" { + emulatorPath := filepath.Join(androidHome, "emulator") + errors = append(errors, fmt.Sprintf("emulator not found. Add %s to PATH", emulatorPath)) + } else { + errors = append(errors, "emulator not found. Install Android Emulator via SDK Manager") + } + } else { + fmt.Println("✓ Android Emulator is installed") + } + + // Check NDK + ndkHome := os.Getenv("ANDROID_NDK_HOME") + if ndkHome == "" && androidHome != "" { + // Look for NDK in default location + ndkDir := filepath.Join(androidHome, "ndk") + if entries, err := os.ReadDir(ndkDir); err == nil { + for _, entry := range entries { + if entry.IsDir() { + ndkHome = filepath.Join(ndkDir, entry.Name()) + break + } + } + } + } + + if ndkHome == "" { + errors = append(errors, "Android NDK not found. Install NDK via Android Studio > SDK Manager > SDK Tools > NDK (Side by side)") + } else { + fmt.Printf("✓ Android NDK: %s\n", ndkHome) + } + + // Check Java + if !checkCommand("java", "-version") { + errors = append(errors, "Java not found. Install JDK 11+ (OpenJDK recommended)") + } else { + fmt.Println("✓ Java is installed") + } + + // Check for AVD (Android Virtual Device) + if checkCommand("emulator", "-list-avds") { + cmd := exec.Command("emulator", "-list-avds") + output, err := cmd.Output() + if err == nil && len(strings.TrimSpace(string(output))) > 0 { + avds := strings.Split(strings.TrimSpace(string(output)), "\n") + fmt.Printf("✓ Found %d Android Virtual Device(s)\n", len(avds)) + } else { + fmt.Println("⚠ No Android Virtual Devices found. Create one via Android Studio > Tools > Device Manager") + } + } + + fmt.Println() + + if len(errors) > 0 { + fmt.Println("❌ Missing dependencies:") + for _, err := range errors { + fmt.Printf(" - %s\n", err) + } + fmt.Println() + fmt.Println("Setup instructions:") + fmt.Println("1. Install Android Studio: https://developer.android.com/studio") + fmt.Println("2. Open SDK Manager and install:") + fmt.Println(" - Android SDK Platform (API 34)") + fmt.Println(" - Android SDK Build-Tools") + fmt.Println(" - Android SDK Platform-Tools") + fmt.Println(" - Android Emulator") + fmt.Println(" - NDK (Side by side)") + fmt.Println("3. Set environment variables:") + if runtime.GOOS == "darwin" { + fmt.Println(" export ANDROID_HOME=$HOME/Library/Android/sdk") + } else { + fmt.Println(" export ANDROID_HOME=$HOME/Android/Sdk") + } + fmt.Println(" export PATH=$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator") + fmt.Println("4. Create an AVD via Android Studio > Tools > Device Manager") + os.Exit(1) + } + + fmt.Println("✓ All Android development dependencies are installed!") +} + +func checkCommand(name string, args ...string) bool { + cmd := exec.Command(name, args...) + cmd.Stdout = nil + cmd.Stderr = nil + return cmd.Run() == nil +} diff --git a/examples/wails3_init_updater/build/android/settings.gradle b/examples/wails3_init_updater/build/android/settings.gradle new file mode 100644 index 0000000..a3f3ec3 --- /dev/null +++ b/examples/wails3_init_updater/build/android/settings.gradle @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "WailsApp" +include ':app' diff --git a/examples/wails3_init_updater/build/appicon.icon/Assets/wails_icon_vector.svg b/examples/wails3_init_updater/build/appicon.icon/Assets/wails_icon_vector.svg new file mode 100644 index 0000000..b099222 --- /dev/null +++ b/examples/wails3_init_updater/build/appicon.icon/Assets/wails_icon_vector.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/examples/wails3_init_updater/build/appicon.icon/icon.json b/examples/wails3_init_updater/build/appicon.icon/icon.json new file mode 100644 index 0000000..5d4fd17 --- /dev/null +++ b/examples/wails3_init_updater/build/appicon.icon/icon.json @@ -0,0 +1,46 @@ +{ + "fill": { + "automatic-gradient": "extended-gray:1.00000,1.00000" + }, + "groups": [ + { + "layers": [ + { + "fill-specializations": [ + { + "appearance": "dark", + "value": { + "solid": "srgb:0.92143,0.92145,0.92144,1.00000" + } + }, + { + "appearance": "tinted", + "value": { + "solid": "srgb:0.83742,0.83744,0.83743,1.00000" + } + } + ], + "image-name": "wails_icon_vector.svg", + "name": "wails_icon_vector", + "position": { + "scale": 1.25, + "translation-in-points": [36.890625, 4.96875] + } + } + ], + "shadow": { + "kind": "neutral", + "opacity": 0.5 + }, + "specular": true, + "translucency": { + "enabled": true, + "value": 0.5 + } + } + ], + "supported-platforms": { + "circles": ["watchOS"], + "squares": "shared" + } +} diff --git a/examples/wails3_init_updater/build/appicon.png b/examples/wails3_init_updater/build/appicon.png new file mode 100644 index 0000000..63617fe Binary files /dev/null and b/examples/wails3_init_updater/build/appicon.png differ diff --git a/examples/wails3_init_updater/build/config.yml b/examples/wails3_init_updater/build/config.yml new file mode 100644 index 0000000..f127864 --- /dev/null +++ b/examples/wails3_init_updater/build/config.yml @@ -0,0 +1,78 @@ +# This file contains the configuration for this project. +# When you update `info` or `fileAssociations`, run `wails3 task common:update:build-assets` to update the assets. +# Note that this will overwrite any changes you have made to the assets. +version: "3" + +# This information is used to generate the build assets. +info: + companyName: "My Company" # The name of the company + productName: "My Product" # The name of the application + productIdentifier: "com.mycompany.myproduct" # The unique product identifier + description: "A program that does X" # The application description + copyright: "(c) 2025, My Company" # Copyright text + comments: "Some Product Comments" # Comments + version: "0.0.1" # The application version + # cfBundleIconName: "appicon" # The macOS icon name in Assets.car icon bundles (optional) + # # Should match the name of your .icon file without the extension + # # If not set and Assets.car exists, defaults to "appicon" + +# iOS build configuration (uncomment to customise iOS project generation) +# Note: Keys under `ios` OVERRIDE values under `info` when set. +# ios: +# # The iOS bundle identifier used in the generated Xcode project (CFBundleIdentifier) +# bundleID: "com.mycompany.myproduct" +# # The display name shown under the app icon (CFBundleDisplayName/CFBundleName) +# displayName: "My Product" +# # The app version to embed in Info.plist (CFBundleShortVersionString/CFBundleVersion) +# version: "0.0.1" +# # The company/organisation name for templates and project settings +# company: "My Company" +# # Additional comments to embed in Info.plist metadata +# comments: "Some Product Comments" + +# Dev mode configuration +dev_mode: + root_path: . + log_level: warn + debounce: 1000 + ignore: + dir: + - .git + - node_modules + - frontend + - bin + file: + - .DS_Store + - .gitignore + - .gitkeep + watched_extension: + - "*.go" + - "*.js" # Watch for changes to JS/TS files included using the //wails:include directive. + - "*.ts" # The frontend directory will be excluded entirely by the setting above. + git_ignore: true + executes: + - cmd: wails3 build DEV=true + type: blocking + - cmd: wails3 task common:dev:frontend + type: background + - cmd: wails3 task run + type: primary + +# File Associations +# More information at: https://v3.wails.io/noit/done/yet +fileAssociations: +# - ext: wails +# name: Wails +# description: Wails Application File +# iconName: wailsFileIcon +# role: Editor +# - ext: jpg +# name: JPEG +# description: Image File +# iconName: jpegFileIcon +# role: Editor +# mimeType: image/jpeg # (optional) + +# Other data +other: + - name: My Other Data diff --git a/examples/wails3_init_updater/build/darwin/Assets.car b/examples/wails3_init_updater/build/darwin/Assets.car new file mode 100644 index 0000000..4def9c3 Binary files /dev/null and b/examples/wails3_init_updater/build/darwin/Assets.car differ diff --git a/examples/wails3_init_updater/build/darwin/Info.dev.plist b/examples/wails3_init_updater/build/darwin/Info.dev.plist new file mode 100644 index 0000000..8759535 --- /dev/null +++ b/examples/wails3_init_updater/build/darwin/Info.dev.plist @@ -0,0 +1,34 @@ + + + + CFBundlePackageType + APPL + CFBundleName + My Product + CFBundleExecutable + updater-example + CFBundleIdentifier + com.example.updaterexample + CFBundleVersion + 0.1.0 + CFBundleGetInfoString + This is a comment + CFBundleShortVersionString + 0.1.0 + CFBundleIconFile + icons + CFBundleIconName + appicon + LSMinimumSystemVersion + 10.15.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + © 2026, My Company + NSAppTransportSecurity + + NSAllowsLocalNetworking + + + + \ No newline at end of file diff --git a/examples/wails3_init_updater/build/darwin/Info.plist b/examples/wails3_init_updater/build/darwin/Info.plist new file mode 100644 index 0000000..4964423 --- /dev/null +++ b/examples/wails3_init_updater/build/darwin/Info.plist @@ -0,0 +1,29 @@ + + + + CFBundlePackageType + APPL + CFBundleName + My Product + CFBundleExecutable + updater-example + CFBundleIdentifier + com.example.updaterexample + CFBundleVersion + 0.1.0 + CFBundleGetInfoString + This is a comment + CFBundleShortVersionString + 0.1.0 + CFBundleIconFile + icons + CFBundleIconName + appicon + LSMinimumSystemVersion + 10.15.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + © 2026, My Company + + \ No newline at end of file diff --git a/examples/wails3_init_updater/build/darwin/Taskfile.yml b/examples/wails3_init_updater/build/darwin/Taskfile.yml new file mode 100644 index 0000000..894d161 --- /dev/null +++ b/examples/wails3_init_updater/build/darwin/Taskfile.yml @@ -0,0 +1,207 @@ +version: "3" + +includes: + common: ../Taskfile.yml + +vars: + # Signing configuration - edit these values for your project + # SIGN_IDENTITY: "Developer ID Application: Your Company (TEAMID)" + # KEYCHAIN_PROFILE: "my-notarize-profile" + # ENTITLEMENTS: "build/darwin/entitlements.plist" + + # Docker image for cross-compilation (used when building on non-macOS) + CROSS_IMAGE: wails-cross + +tasks: + build: + summary: Builds the application + cmds: + - task: '{{if eq OS "darwin"}}build:native{{else}}build:docker{{end}}' + vars: + ARCH: "{{.ARCH}}" + DEV: "{{.DEV}}" + OUTPUT: "{{.OUTPUT}}" + EXTRA_TAGS: "{{.EXTRA_TAGS}}" + vars: + DEFAULT_OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}" + OUTPUT: "{{ .OUTPUT | default .DEFAULT_OUTPUT }}" + + build:native: + summary: Builds the application natively on macOS + internal: true + deps: + - task: common:go:mod:tidy + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + DEV: + ref: .DEV + - task: common:generate:icons + cmds: + - go build {{.BUILD_FLAGS}} -o {{.OUTPUT}} + vars: + BUILD_FLAGS: '{{if eq .DEV "true"}}{{if .EXTRA_TAGS}}-tags {{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s"{{end}}' + DEFAULT_OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}" + OUTPUT: "{{ .OUTPUT | default .DEFAULT_OUTPUT }}" + env: + GOOS: darwin + CGO_ENABLED: 1 + GOARCH: "{{.ARCH | default ARCH}}" + CGO_CFLAGS: "-mmacosx-version-min=10.15" + CGO_LDFLAGS: "-mmacosx-version-min=10.15" + MACOSX_DEPLOYMENT_TARGET: "10.15" + + build:docker: + summary: Cross-compiles for macOS using Docker (for Linux/Windows hosts) + internal: true + deps: + - task: common:build:frontend + - task: common:generate:icons + preconditions: + - sh: docker info > /dev/null 2>&1 + msg: "Docker is required for cross-compilation. Please install Docker." + - sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1 + msg: | + Docker image '{{.CROSS_IMAGE}}' not found. + Build it first: wails3 task setup:docker + cmds: + - docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{if .EXTRA_TAGS}}-e EXTRA_TAGS="{{.EXTRA_TAGS}}"{{end}} {{.CROSS_IMAGE}} darwin {{.DOCKER_ARCH}} + - docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin + - mkdir -p {{.BIN_DIR}} + - mv "bin/{{.APP_NAME}}-darwin-{{.DOCKER_ARCH}}" "{{.OUTPUT}}" + vars: + DOCKER_ARCH: '{{if eq .ARCH "arm64"}}arm64{{else if eq .ARCH "amd64"}}amd64{{else}}arm64{{end}}' + DEFAULT_OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}" + OUTPUT: "{{ .OUTPUT | default .DEFAULT_OUTPUT }}" + # Mount Go module cache for faster builds + GO_CACHE_MOUNT: + sh: 'echo "-v ${GOPATH:-$HOME/go}/pkg/mod:/go/pkg/mod"' + # Extract replace directives from go.mod and create -v mounts for each + # Handles both relative (=> ../) and absolute (=> /) paths + REPLACE_MOUNTS: + sh: | + grep -E '^replace .* => ' go.mod 2>/dev/null | while read -r line; do + path=$(echo "$line" | sed -E 's/^replace .* => //' | tr -d '\r') + # Convert relative paths to absolute + if [ "${path#/}" = "$path" ]; then + path="$(cd "$(dirname "$path")" 2>/dev/null && pwd)/$(basename "$path")" + fi + # Only mount if directory exists + if [ -d "$path" ]; then + echo "-v $path:$path:ro" + fi + done | tr '\n' ' ' + + build:universal: + summary: Builds darwin universal binary (arm64 + amd64) + deps: + - task: build + vars: + ARCH: amd64 + OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" + - task: build + vars: + ARCH: arm64 + OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + cmds: + - task: '{{if eq OS "darwin"}}build:universal:lipo:native{{else}}build:universal:lipo:go{{end}}' + + build:universal:lipo:native: + summary: Creates universal binary using native lipo (macOS) + internal: true + cmds: + - lipo -create -output "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + - rm "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + + build:universal:lipo:go: + summary: Creates universal binary using wails3 tool lipo (Linux/Windows) + internal: true + cmds: + - wails3 tool lipo -output "{{.BIN_DIR}}/{{.APP_NAME}}" -input "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" -input "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + - rm -f "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + + package: + summary: Packages the application into a `.app` bundle + deps: + - task: build + cmds: + - task: create:app:bundle + + package:universal: + summary: Packages darwin universal binary (arm64 + amd64) + deps: + - task: build:universal + cmds: + - task: create:app:bundle + + create:app:bundle: + summary: Creates an `.app` bundle + cmds: + - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS" + - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources" + - cp build/darwin/icons.icns "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources" + - | + if [ -f build/darwin/Assets.car ]; then + cp build/darwin/Assets.car "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources" + fi + - cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS" + - cp build/darwin/Info.plist "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents" + - task: '{{if eq OS "darwin"}}codesign:adhoc{{else}}codesign:skip{{end}}' + + codesign:adhoc: + summary: Ad-hoc signs the app bundle (macOS only) + internal: true + cmds: + - codesign --force --deep --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.app" + + codesign:skip: + summary: Skips codesigning when cross-compiling + internal: true + cmds: + - 'echo "Skipping codesign (not available on {{OS}}). Sign the .app on macOS before distribution."' + + run: + cmds: + - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS" + - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources" + - cp build/darwin/icons.icns "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources" + - | + if [ -f build/darwin/Assets.car ]; then + cp build/darwin/Assets.car "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources" + fi + - cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS" + - cp "build/darwin/Info.dev.plist" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Info.plist" + - codesign --force --deep --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app" + - "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS/{{.APP_NAME}}" + + sign: + summary: Signs the application bundle with Developer ID + desc: | + Signs the .app bundle for distribution. + Configure SIGN_IDENTITY in the vars section at the top of this file. + deps: + - task: package + cmds: + - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.app" --identity "{{.SIGN_IDENTITY}}" {{if .ENTITLEMENTS}}--entitlements {{.ENTITLEMENTS}}{{end}} + preconditions: + - sh: '[ -n "{{.SIGN_IDENTITY}}" ]' + msg: "SIGN_IDENTITY is required. Set it in the vars section at the top of build/darwin/Taskfile.yml" + + sign:notarize: + summary: Signs and notarizes the application bundle + desc: | + Signs the .app bundle and submits it for notarization. + Configure SIGN_IDENTITY and KEYCHAIN_PROFILE in the vars section at the top of this file. + + Setup (one-time): + wails3 signing credentials --apple-id "you@email.com" --team-id "TEAMID" --password "app-specific-password" --profile "my-profile" + deps: + - task: package + cmds: + - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.app" --identity "{{.SIGN_IDENTITY}}" {{if .ENTITLEMENTS}}--entitlements {{.ENTITLEMENTS}}{{end}} --notarize --keychain-profile {{.KEYCHAIN_PROFILE}} + preconditions: + - sh: '[ -n "{{.SIGN_IDENTITY}}" ]' + msg: "SIGN_IDENTITY is required. Set it in the vars section at the top of build/darwin/Taskfile.yml" + - sh: '[ -n "{{.KEYCHAIN_PROFILE}}" ]' + msg: "KEYCHAIN_PROFILE is required. Set it in the vars section at the top of build/darwin/Taskfile.yml" diff --git a/examples/wails3_init_updater/build/darwin/icons.icns b/examples/wails3_init_updater/build/darwin/icons.icns new file mode 100644 index 0000000..458ce19 Binary files /dev/null and b/examples/wails3_init_updater/build/darwin/icons.icns differ diff --git a/examples/wails3_init_updater/build/docker/Dockerfile.cross b/examples/wails3_init_updater/build/docker/Dockerfile.cross new file mode 100644 index 0000000..a3c01f2 --- /dev/null +++ b/examples/wails3_init_updater/build/docker/Dockerfile.cross @@ -0,0 +1,203 @@ +# Cross-compile Wails v3 apps to any platform +# +# Darwin: Zig + macOS SDK +# Linux: Native GCC when host matches target, Zig for cross-arch +# Windows: Zig + bundled mingw +# +# Usage: +# docker build -t wails-cross -f Dockerfile.cross . +# docker run --rm -v $(pwd):/app wails-cross darwin arm64 +# docker run --rm -v $(pwd):/app wails-cross darwin amd64 +# docker run --rm -v $(pwd):/app wails-cross linux amd64 +# docker run --rm -v $(pwd):/app wails-cross linux arm64 +# docker run --rm -v $(pwd):/app wails-cross windows amd64 +# docker run --rm -v $(pwd):/app wails-cross windows arm64 + +FROM golang:1.25-bookworm + +ARG TARGETARCH + +# Install base tools, GCC, and GTK/WebKit dev packages +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl xz-utils nodejs npm pkg-config gcc libc6-dev \ + libgtk-3-dev libwebkit2gtk-4.1-dev \ + libgtk-4-dev libwebkitgtk-6.0-dev \ + && rm -rf /var/lib/apt/lists/* + +# Install Zig - automatically selects correct binary for host architecture +ARG ZIG_VERSION=0.14.0 +RUN ZIG_ARCH=$(case "${TARGETARCH}" in arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \ + curl -L "https://ziglang.org/download/${ZIG_VERSION}/zig-linux-${ZIG_ARCH}-${ZIG_VERSION}.tar.xz" \ + | tar -xJ -C /opt \ + && ln -s /opt/zig-linux-${ZIG_ARCH}-${ZIG_VERSION}/zig /usr/local/bin/zig + +# Download macOS SDK (required for darwin targets) +ARG MACOS_SDK_VERSION=14.5 +RUN curl -L "https://github.com/joseluisq/macosx-sdks/releases/download/${MACOS_SDK_VERSION}/MacOSX${MACOS_SDK_VERSION}.sdk.tar.xz" \ + | tar -xJ -C /opt \ + && mv /opt/MacOSX${MACOS_SDK_VERSION}.sdk /opt/macos-sdk + +ENV MACOS_SDK_PATH=/opt/macos-sdk + +# Create Zig CC wrappers for cross-compilation targets +# Darwin and Windows use Zig; Linux uses native GCC (run with --platform for cross-arch) + +# Darwin arm64 +COPY <<'ZIGWRAP' /usr/local/bin/zcc-darwin-arm64 +#!/bin/sh +ARGS="" +SKIP_NEXT=0 +for arg in "$@"; do + if [ $SKIP_NEXT -eq 1 ]; then + SKIP_NEXT=0 + continue + fi + case "$arg" in + -target) SKIP_NEXT=1 ;; + -mmacosx-version-min=*) ;; + *) ARGS="$ARGS $arg" ;; + esac +done +exec zig cc -fno-sanitize=all -target aarch64-macos-none -isysroot /opt/macos-sdk -I/opt/macos-sdk/usr/include -L/opt/macos-sdk/usr/lib -F/opt/macos-sdk/System/Library/Frameworks -w $ARGS +ZIGWRAP +RUN chmod +x /usr/local/bin/zcc-darwin-arm64 + +# Darwin amd64 +COPY <<'ZIGWRAP' /usr/local/bin/zcc-darwin-amd64 +#!/bin/sh +ARGS="" +SKIP_NEXT=0 +for arg in "$@"; do + if [ $SKIP_NEXT -eq 1 ]; then + SKIP_NEXT=0 + continue + fi + case "$arg" in + -target) SKIP_NEXT=1 ;; + -mmacosx-version-min=*) ;; + *) ARGS="$ARGS $arg" ;; + esac +done +exec zig cc -fno-sanitize=all -target x86_64-macos-none -isysroot /opt/macos-sdk -I/opt/macos-sdk/usr/include -L/opt/macos-sdk/usr/lib -F/opt/macos-sdk/System/Library/Frameworks -w $ARGS +ZIGWRAP +RUN chmod +x /usr/local/bin/zcc-darwin-amd64 + +# Windows amd64 - uses Zig's bundled mingw +COPY <<'ZIGWRAP' /usr/local/bin/zcc-windows-amd64 +#!/bin/sh +ARGS="" +SKIP_NEXT=0 +for arg in "$@"; do + if [ $SKIP_NEXT -eq 1 ]; then + SKIP_NEXT=0 + continue + fi + case "$arg" in + -target) SKIP_NEXT=1 ;; + -Wl,*) ;; + *) ARGS="$ARGS $arg" ;; + esac +done +exec zig cc -target x86_64-windows-gnu $ARGS +ZIGWRAP +RUN chmod +x /usr/local/bin/zcc-windows-amd64 + +# Windows arm64 - uses Zig's bundled mingw +COPY <<'ZIGWRAP' /usr/local/bin/zcc-windows-arm64 +#!/bin/sh +ARGS="" +SKIP_NEXT=0 +for arg in "$@"; do + if [ $SKIP_NEXT -eq 1 ]; then + SKIP_NEXT=0 + continue + fi + case "$arg" in + -target) SKIP_NEXT=1 ;; + -Wl,*) ;; + *) ARGS="$ARGS $arg" ;; + esac +done +exec zig cc -target aarch64-windows-gnu $ARGS +ZIGWRAP +RUN chmod +x /usr/local/bin/zcc-windows-arm64 + +# Build script +COPY <<'SCRIPT' /usr/local/bin/build.sh +#!/bin/sh +set -e + +OS=${1:-darwin} +ARCH=${2:-arm64} + +case "${OS}-${ARCH}" in + darwin-arm64|darwin-aarch64) + export CC=zcc-darwin-arm64 + export GOARCH=arm64 + export GOOS=darwin + ;; + darwin-amd64|darwin-x86_64) + export CC=zcc-darwin-amd64 + export GOARCH=amd64 + export GOOS=darwin + ;; + linux-arm64|linux-aarch64) + export CC=gcc + export GOARCH=arm64 + export GOOS=linux + ;; + linux-amd64|linux-x86_64) + export CC=gcc + export GOARCH=amd64 + export GOOS=linux + ;; + windows-arm64|windows-aarch64) + export CC=zcc-windows-arm64 + export GOARCH=arm64 + export GOOS=windows + ;; + windows-amd64|windows-x86_64) + export CC=zcc-windows-amd64 + export GOARCH=amd64 + export GOOS=windows + ;; + *) + echo "Usage: " + echo " os: darwin, linux, windows" + echo " arch: amd64, arm64" + exit 1 + ;; +esac + +export CGO_ENABLED=1 +export CGO_CFLAGS="-w" + +# Build frontend if exists and not already built (host may have built it) +if [ -d "frontend" ] && [ -f "frontend/package.json" ] && [ ! -d "frontend/dist" ]; then + (cd frontend && npm install --silent && npm run build --silent) +fi + +# Build +APP=${APP_NAME:-$(basename $(pwd))} +mkdir -p bin + +EXT="" +LDFLAGS="-s -w" +if [ "$GOOS" = "windows" ]; then + EXT=".exe" + LDFLAGS="-s -w -H windowsgui" +fi + +TAGS="production" +if [ -n "$EXTRA_TAGS" ]; then + TAGS="${TAGS},${EXTRA_TAGS}" +fi + +go build -tags "$TAGS" -trimpath -buildvcs=false -ldflags="$LDFLAGS" -o bin/${APP}-${GOOS}-${GOARCH}${EXT} . +echo "Built: bin/${APP}-${GOOS}-${GOARCH}${EXT}" +SCRIPT +RUN chmod +x /usr/local/bin/build.sh + +WORKDIR /app +ENTRYPOINT ["/usr/local/bin/build.sh"] +CMD ["darwin", "arm64"] diff --git a/examples/wails3_init_updater/build/docker/Dockerfile.server b/examples/wails3_init_updater/build/docker/Dockerfile.server new file mode 100644 index 0000000..58fb64f --- /dev/null +++ b/examples/wails3_init_updater/build/docker/Dockerfile.server @@ -0,0 +1,41 @@ +# Wails Server Mode Dockerfile +# Multi-stage build for minimal image size + +# Build stage +FROM golang:alpine AS builder + +WORKDIR /app + +# Install build dependencies +RUN apk add --no-cache git + +# Copy source code +COPY . . + +# Remove local replace directive if present (for production builds) +RUN sed -i '/^replace/d' go.mod || true + +# Download dependencies +RUN go mod tidy + +# Build the server binary +RUN go build -tags server -ldflags="-s -w" -o server . + +# Runtime stage - minimal image +FROM gcr.io/distroless/static-debian12 + +# Copy the binary +COPY --from=builder /app/server /server + +# Copy frontend assets +COPY --from=builder /app/frontend/dist /frontend/dist + +# Expose the default port +EXPOSE 8080 + +# Bind to all interfaces (required for Docker) +# Can be overridden at runtime with -e WAILS_SERVER_HOST=... +ENV WAILS_SERVER_HOST=0.0.0.0 + +# Run the server +ENTRYPOINT ["/server"] diff --git a/examples/wails3_init_updater/build/ios/Assets.xcassets b/examples/wails3_init_updater/build/ios/Assets.xcassets new file mode 100644 index 0000000..46fbb87 --- /dev/null +++ b/examples/wails3_init_updater/build/ios/Assets.xcassets @@ -0,0 +1,116 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "images" : [ + { + "filename" : "icon-20@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "icon-20@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "icon-29@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "icon-29@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "icon-40@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "icon-40@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "icon-60@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "icon-60@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "icon-20.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "filename" : "icon-20@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "icon-29.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "icon-29@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "icon-40.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "filename" : "icon-40@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "icon-76.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "icon-76@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "icon-83.5@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "icon-1024.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ] +} \ No newline at end of file diff --git a/examples/wails3_init_updater/build/ios/Info.dev.plist b/examples/wails3_init_updater/build/ios/Info.dev.plist new file mode 100644 index 0000000..d6d5fce --- /dev/null +++ b/examples/wails3_init_updater/build/ios/Info.dev.plist @@ -0,0 +1,62 @@ + + + + + CFBundleExecutable + updater-example + CFBundleIdentifier + com.example.updaterexample.dev + CFBundleName + My Product (Dev) + CFBundleDisplayName + My Product (Dev) + CFBundlePackageType + APPL + CFBundleShortVersionString + 0.1.0-dev + CFBundleVersion + 0.1.0 + LSRequiresIPhoneOS + + MinimumOSVersion + 15.0 + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAllowsLocalNetworking + + + + WailsDevelopmentMode + + + NSHumanReadableCopyright + © 2026, My Company + + + CFBundleGetInfoString + This is a comment + + + \ No newline at end of file diff --git a/examples/wails3_init_updater/build/ios/Info.plist b/examples/wails3_init_updater/build/ios/Info.plist new file mode 100644 index 0000000..522122f --- /dev/null +++ b/examples/wails3_init_updater/build/ios/Info.plist @@ -0,0 +1,59 @@ + + + + + CFBundleExecutable + updater-example + CFBundleIdentifier + com.example.updaterexample + CFBundleName + My Product + CFBundleDisplayName + My Product + CFBundlePackageType + APPL + CFBundleShortVersionString + 0.1.0 + CFBundleVersion + 0.1.0 + LSRequiresIPhoneOS + + MinimumOSVersion + 15.0 + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAllowsLocalNetworking + + + + NSHumanReadableCopyright + © 2026, My Company + + + CFBundleGetInfoString + This is a comment + + + \ No newline at end of file diff --git a/examples/wails3_init_updater/build/ios/LaunchScreen.storyboard b/examples/wails3_init_updater/build/ios/LaunchScreen.storyboard new file mode 100644 index 0000000..6a23f39 --- /dev/null +++ b/examples/wails3_init_updater/build/ios/LaunchScreen.storyboard @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/wails3_init_updater/build/ios/Taskfile.yml b/examples/wails3_init_updater/build/ios/Taskfile.yml new file mode 100644 index 0000000..ebb09c4 --- /dev/null +++ b/examples/wails3_init_updater/build/ios/Taskfile.yml @@ -0,0 +1,293 @@ +version: "3" + +includes: + common: ../Taskfile.yml + +vars: + BUNDLE_ID: '{{.BUNDLE_ID | default "com.wails.app"}}' + # SDK_PATH is computed lazily at task-level to avoid errors on non-macOS systems + # Each task that needs it defines SDK_PATH in its own vars section + +tasks: + install:deps: + summary: Check and install iOS development dependencies + cmds: + - go run build/ios/scripts/deps/install_deps.go + env: + TASK_FORCE_YES: "{{if .YES}}true{{else}}false{{end}}" + prompt: This will check and install iOS development dependencies. Continue? + + # Note: Bindings generation may show CGO warnings for iOS C imports. + # These warnings are harmless and don't affect the generated bindings, + # as the generator only needs to parse Go types, not C implementations. + build: + summary: Creates a build of the application for iOS + deps: + - task: generate:ios:overlay + - task: generate:ios:xcode + - task: common:go:mod:tidy + - task: generate:ios:bindings + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + PRODUCTION: + ref: .PRODUCTION + - task: common:generate:icons + cmds: + - echo "Building iOS app {{.APP_NAME}}..." + - go build -buildmode=c-archive -overlay build/ios/xcode/overlay.json {{.BUILD_FLAGS}} -o {{.OUTPUT}}.a + vars: + BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production,ios -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-tags ios,debug -buildvcs=false -gcflags=all="-l"{{end}}' + DEFAULT_OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}" + OUTPUT: "{{ .OUTPUT | default .DEFAULT_OUTPUT }}" + SDK_PATH: + sh: xcrun --sdk iphonesimulator --show-sdk-path + env: + GOOS: ios + CGO_ENABLED: 1 + GOARCH: '{{.ARCH | default "arm64"}}' + PRODUCTION: '{{.PRODUCTION | default "false"}}' + CGO_CFLAGS: "-isysroot {{.SDK_PATH}} -target arm64-apple-ios15.0-simulator -mios-simulator-version-min=15.0" + CGO_LDFLAGS: "-isysroot {{.SDK_PATH}} -target arm64-apple-ios15.0-simulator" + + compile:objc: + summary: Compile Objective-C iOS wrapper + vars: + SDK_PATH: + sh: xcrun --sdk iphonesimulator --show-sdk-path + cmds: + - xcrun -sdk iphonesimulator clang -target arm64-apple-ios15.0-simulator -isysroot {{.SDK_PATH}} -framework Foundation -framework UIKit -framework WebKit -o {{.BIN_DIR}}/{{.APP_NAME}} build/ios/main.m + - codesign --force --sign - "{{.BIN_DIR}}/{{.APP_NAME}}" + + package: + summary: Packages a production build of the application into a `.app` bundle + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: create:app:bundle + + create:app:bundle: + summary: Creates an iOS `.app` bundle + cmds: + - rm -rf "{{.BIN_DIR}}/{{.APP_NAME}}.app" + - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app" + - cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.app/" + - cp build/ios/Info.plist "{{.BIN_DIR}}/{{.APP_NAME}}.app/" + - | + # Compile asset catalog and embed icons in the app bundle + APP_BUNDLE="{{.BIN_DIR}}/{{.APP_NAME}}.app" + AC_IN="build/ios/xcode/main/Assets.xcassets" + if [ -d "$AC_IN" ]; then + TMP_AC=$(mktemp -d) + xcrun actool \ + --compile "$TMP_AC" \ + --app-icon AppIcon \ + --platform iphonesimulator \ + --minimum-deployment-target 15.0 \ + --product-type com.apple.product-type.application \ + --target-device iphone \ + --target-device ipad \ + --output-partial-info-plist "$APP_BUNDLE/assetcatalog_generated_info.plist" \ + "$AC_IN" + if [ -f "$TMP_AC/Assets.car" ]; then + cp -f "$TMP_AC/Assets.car" "$APP_BUNDLE/Assets.car" + fi + rm -rf "$TMP_AC" + if [ -f "$APP_BUNDLE/assetcatalog_generated_info.plist" ]; then + /usr/libexec/PlistBuddy -c "Merge $APP_BUNDLE/assetcatalog_generated_info.plist" "$APP_BUNDLE/Info.plist" || true + fi + fi + - codesign --force --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.app" + + deploy-simulator: + summary: Deploy to iOS Simulator + deps: [package] + cmds: + - xcrun simctl terminate booted {{.BUNDLE_ID}} 2>/dev/null || true + - xcrun simctl uninstall booted {{.BUNDLE_ID}} 2>/dev/null || true + - xcrun simctl install booted "{{.BIN_DIR}}/{{.APP_NAME}}.app" + - xcrun simctl launch booted {{.BUNDLE_ID}} + + compile:ios: + summary: Compile the iOS executable from Go archive and main.m + deps: + - task: build + vars: + SDK_PATH: + sh: xcrun --sdk iphonesimulator --show-sdk-path + cmds: + - | + MAIN_M=build/ios/xcode/main/main.m + if [ ! -f "$MAIN_M" ]; then + MAIN_M=build/ios/main.m + fi + xcrun -sdk iphonesimulator clang \ + -target arm64-apple-ios15.0-simulator \ + -isysroot {{.SDK_PATH}} \ + -framework Foundation -framework UIKit -framework WebKit \ + -framework Security -framework CoreFoundation \ + -lresolv \ + -o "{{.BIN_DIR}}/{{.APP_NAME | lower}}" \ + "$MAIN_M" "{{.BIN_DIR}}/{{.APP_NAME}}.a" + + generate:ios:bindings: + internal: true + summary: Generates bindings for iOS with proper CGO flags + sources: + - "**/*.go" + - go.mod + - go.sum + generates: + - frontend/bindings/**/* + vars: + SDK_PATH: + sh: xcrun --sdk iphonesimulator --show-sdk-path + cmds: + - wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true + env: + GOOS: ios + CGO_ENABLED: 1 + GOARCH: '{{.ARCH | default "arm64"}}' + CGO_CFLAGS: "-isysroot {{.SDK_PATH}} -target arm64-apple-ios15.0-simulator -mios-simulator-version-min=15.0" + CGO_LDFLAGS: "-isysroot {{.SDK_PATH}} -target arm64-apple-ios15.0-simulator" + + ensure-simulator: + internal: true + summary: Ensure iOS Simulator is running and booted + silent: true + cmds: + - | + if ! xcrun simctl list devices booted | grep -q "Booted"; then + echo "Starting iOS Simulator..." + # Get first available iPhone device + DEVICE_ID=$(xcrun simctl list devices available | grep "iPhone" | head -1 | grep -o "[A-F0-9-]\{36\}" || true) + if [ -z "$DEVICE_ID" ]; then + echo "No iPhone simulator found. Creating one..." + RUNTIME=$(xcrun simctl list runtimes | grep iOS | tail -1 | awk '{print $NF}') + DEVICE_ID=$(xcrun simctl create "iPhone 15 Pro" "iPhone 15 Pro" "$RUNTIME") + fi + # Boot the device + echo "Booting device $DEVICE_ID..." + xcrun simctl boot "$DEVICE_ID" 2>/dev/null || true + # Open Simulator app + open -a Simulator + # Wait for boot (max 30 seconds) + for i in {1..30}; do + if xcrun simctl list devices booted | grep -q "Booted"; then + echo "Simulator booted successfully" + break + fi + sleep 1 + done + # Final check + if ! xcrun simctl list devices booted | grep -q "Booted"; then + echo "Failed to boot simulator after 30 seconds" + exit 1 + fi + fi + preconditions: + - sh: command -v xcrun + msg: "xcrun not found. Please run 'wails3 task ios:install:deps' to install iOS development dependencies" + + generate:ios:overlay: + internal: true + summary: Generate Go build overlay and iOS shim + sources: + - build/config.yml + generates: + - build/ios/xcode/overlay.json + - build/ios/xcode/gen/main_ios.gen.go + cmds: + - wails3 ios overlay:gen -out build/ios/xcode/overlay.json -config build/config.yml + + generate:ios:xcode: + internal: true + summary: Generate iOS Xcode project structure and assets + sources: + - build/config.yml + - build/appicon.png + generates: + - build/ios/xcode/main/main.m + - build/ios/xcode/main/Assets.xcassets/**/* + - build/ios/xcode/project.pbxproj + cmds: + - wails3 ios xcode:gen -outdir build/ios/xcode -config build/config.yml + + run: + summary: Run the application in iOS Simulator + deps: + - task: ensure-simulator + - task: compile:ios + cmds: + - rm -rf "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app" + - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app" + - cp "{{.BIN_DIR}}/{{.APP_NAME | lower}}" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/{{.APP_NAME | lower}}" + - cp build/ios/Info.dev.plist "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Info.plist" + - | + # Compile asset catalog and embed icons for dev bundle + APP_BUNDLE="{{.BIN_DIR}}/{{.APP_NAME}}.dev.app" + AC_IN="build/ios/xcode/main/Assets.xcassets" + if [ -d "$AC_IN" ]; then + TMP_AC=$(mktemp -d) + xcrun actool \ + --compile "$TMP_AC" \ + --app-icon AppIcon \ + --platform iphonesimulator \ + --minimum-deployment-target 15.0 \ + --product-type com.apple.product-type.application \ + --target-device iphone \ + --target-device ipad \ + --output-partial-info-plist "$APP_BUNDLE/assetcatalog_generated_info.plist" \ + "$AC_IN" + if [ -f "$TMP_AC/Assets.car" ]; then + cp -f "$TMP_AC/Assets.car" "$APP_BUNDLE/Assets.car" + fi + rm -rf "$TMP_AC" + if [ -f "$APP_BUNDLE/assetcatalog_generated_info.plist" ]; then + /usr/libexec/PlistBuddy -c "Merge $APP_BUNDLE/assetcatalog_generated_info.plist" "$APP_BUNDLE/Info.plist" || true + fi + fi + - codesign --force --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app" + - xcrun simctl terminate booted "com.wails.{{.APP_NAME | lower}}.dev" 2>/dev/null || true + - xcrun simctl uninstall booted "com.wails.{{.APP_NAME | lower}}.dev" 2>/dev/null || true + - xcrun simctl install booted "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app" + - xcrun simctl launch booted "com.wails.{{.APP_NAME | lower}}.dev" + + xcode: + summary: Open the generated Xcode project for this app + cmds: + - task: generate:ios:xcode + - open build/ios/xcode/main.xcodeproj + + logs: + summary: Stream iOS Simulator logs filtered to this app + cmds: + - | + xcrun simctl spawn booted log stream \ + --level debug \ + --style compact \ + --predicate 'senderImagePath CONTAINS[c] "{{.APP_NAME | lower}}.app/" OR composedMessage CONTAINS[c] "{{.APP_NAME | lower}}" OR eventMessage CONTAINS[c] "{{.APP_NAME | lower}}" OR process == "{{.APP_NAME | lower}}" OR category CONTAINS[c] "{{.APP_NAME | lower}}"' + + logs:dev: + summary: Stream logs for the dev bundle (used by `task ios:run`) + cmds: + - | + xcrun simctl spawn booted log stream \ + --level debug \ + --style compact \ + --predicate 'senderImagePath CONTAINS[c] ".dev.app/" OR subsystem == "com.wails.{{.APP_NAME | lower}}.dev" OR process == "{{.APP_NAME | lower}}"' + + logs:wide: + summary: Wide log stream to help discover the exact process/bundle identifiers + cmds: + - | + xcrun simctl spawn booted log stream \ + --level debug \ + --style compact \ + --predicate 'senderImagePath CONTAINS[c] ".app/"' diff --git a/examples/wails3_init_updater/build/ios/app_options_default.go b/examples/wails3_init_updater/build/ios/app_options_default.go new file mode 100644 index 0000000..04e4f1b --- /dev/null +++ b/examples/wails3_init_updater/build/ios/app_options_default.go @@ -0,0 +1,10 @@ +//go:build !ios + +package main + +import "github.com/wailsapp/wails/v3/pkg/application" + +// modifyOptionsForIOS is a no-op on non-iOS platforms +func modifyOptionsForIOS(opts *application.Options) { + // No modifications needed for non-iOS platforms +} \ No newline at end of file diff --git a/examples/wails3_init_updater/build/ios/app_options_ios.go b/examples/wails3_init_updater/build/ios/app_options_ios.go new file mode 100644 index 0000000..8f6ac31 --- /dev/null +++ b/examples/wails3_init_updater/build/ios/app_options_ios.go @@ -0,0 +1,11 @@ +//go:build ios + +package main + +import "github.com/wailsapp/wails/v3/pkg/application" + +// modifyOptionsForIOS adjusts the application options for iOS +func modifyOptionsForIOS(opts *application.Options) { + // Disable signal handlers on iOS to prevent crashes + opts.DisableDefaultSignalHandler = true +} \ No newline at end of file diff --git a/examples/wails3_init_updater/build/ios/build.sh b/examples/wails3_init_updater/build/ios/build.sh new file mode 100644 index 0000000..250c7b5 --- /dev/null +++ b/examples/wails3_init_updater/build/ios/build.sh @@ -0,0 +1,72 @@ +#!/bin/bash +set -e + +# Build configuration +APP_NAME="updater-example" +BUNDLE_ID="com.example.updaterexample" +VERSION="0.1.0" +BUILD_NUMBER="0.1.0" +BUILD_DIR="build/ios" +TARGET="simulator" + +echo "Building iOS app: $APP_NAME" +echo "Bundle ID: $BUNDLE_ID" +echo "Version: $VERSION ($BUILD_NUMBER)" +echo "Target: $TARGET" + +# Ensure build directory exists +mkdir -p "$BUILD_DIR" + +# Determine SDK and target architecture +if [ "$TARGET" = "simulator" ]; then + SDK="iphonesimulator" + ARCH="arm64-apple-ios15.0-simulator" +elif [ "$TARGET" = "device" ]; then + SDK="iphoneos" + ARCH="arm64-apple-ios15.0" +else + echo "Unknown target: $TARGET" + exit 1 +fi + +# Get SDK path +SDK_PATH=$(xcrun --sdk $SDK --show-sdk-path) + +# Compile the application +echo "Compiling with SDK: $SDK" +xcrun -sdk $SDK clang \ + -target $ARCH \ + -isysroot "$SDK_PATH" \ + -framework Foundation \ + -framework UIKit \ + -framework WebKit \ + -framework CoreGraphics \ + -o "$BUILD_DIR/$APP_NAME" \ + "$BUILD_DIR/main.m" + +# Create app bundle +echo "Creating app bundle..." +APP_BUNDLE="$BUILD_DIR/$APP_NAME.app" +rm -rf "$APP_BUNDLE" +mkdir -p "$APP_BUNDLE" + +# Move executable +mv "$BUILD_DIR/$APP_NAME" "$APP_BUNDLE/" + +# Copy Info.plist +cp "$BUILD_DIR/Info.plist" "$APP_BUNDLE/" + +# Sign the app +echo "Signing app..." +codesign --force --sign - "$APP_BUNDLE" + +echo "Build complete: $APP_BUNDLE" + +# Deploy to simulator if requested +if [ "$TARGET" = "simulator" ]; then + echo "Deploying to simulator..." + xcrun simctl terminate booted "$BUNDLE_ID" 2>/dev/null || true + xcrun simctl install booted "$APP_BUNDLE" + xcrun simctl launch booted "$BUNDLE_ID" + echo "App launched on simulator" +fi diff --git a/examples/wails3_init_updater/build/ios/entitlements.plist b/examples/wails3_init_updater/build/ios/entitlements.plist new file mode 100644 index 0000000..cc5d958 --- /dev/null +++ b/examples/wails3_init_updater/build/ios/entitlements.plist @@ -0,0 +1,21 @@ + + + + + + get-task-allow + + + + com.apple.security.app-sandbox + + + + com.apple.security.network.client + + + + com.apple.security.files.user-selected.read-only + + + \ No newline at end of file diff --git a/examples/wails3_init_updater/build/ios/icon.png b/examples/wails3_init_updater/build/ios/icon.png new file mode 100644 index 0000000..be7d591 --- /dev/null +++ b/examples/wails3_init_updater/build/ios/icon.png @@ -0,0 +1,3 @@ +# iOS Icon Placeholder +# This file should be replaced with the actual app icon (1024x1024 PNG) +# The build process will generate all required icon sizes from this base icon \ No newline at end of file diff --git a/examples/wails3_init_updater/build/ios/main.m b/examples/wails3_init_updater/build/ios/main.m new file mode 100644 index 0000000..366767a --- /dev/null +++ b/examples/wails3_init_updater/build/ios/main.m @@ -0,0 +1,23 @@ +//go:build ios +// Minimal bootstrap: delegate comes from Go archive (WailsAppDelegate) +#import +#include + +// External Go initialization function from the c-archive (declare before use) +extern void WailsIOSMain(); + +int main(int argc, char * argv[]) { + @autoreleasepool { + // Disable buffering so stdout/stderr from Go log.Printf flush immediately + setvbuf(stdout, NULL, _IONBF, 0); + setvbuf(stderr, NULL, _IONBF, 0); + + // Start Go runtime on a background queue to avoid blocking main thread/UI + dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ + WailsIOSMain(); + }); + + // Run UIApplicationMain using WailsAppDelegate provided by the Go archive + return UIApplicationMain(argc, argv, nil, @"WailsAppDelegate"); + } +} \ No newline at end of file diff --git a/examples/wails3_init_updater/build/ios/main_ios.go b/examples/wails3_init_updater/build/ios/main_ios.go new file mode 100644 index 0000000..b75a403 --- /dev/null +++ b/examples/wails3_init_updater/build/ios/main_ios.go @@ -0,0 +1,24 @@ +//go:build ios + +package main + +import ( + "C" +) + +// For iOS builds, we need to export a function that can be called from Objective-C +// This wrapper allows us to keep the original main.go unmodified + +//export WailsIOSMain +func WailsIOSMain() { + // DO NOT lock the goroutine to the current OS thread on iOS! + // This causes signal handling issues: + // "signal 16 received on thread with no signal stack" + // "fatal error: non-Go code disabled sigaltstack" + // iOS apps run in a sandboxed environment where the Go runtime's + // signal handling doesn't work the same way as desktop platforms. + + // Call the actual main function from main.go + // This ensures all the user's code is executed + main() +} \ No newline at end of file diff --git a/examples/wails3_init_updater/build/ios/project.pbxproj b/examples/wails3_init_updater/build/ios/project.pbxproj new file mode 100644 index 0000000..6488bda --- /dev/null +++ b/examples/wails3_init_updater/build/ios/project.pbxproj @@ -0,0 +1,222 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = {}; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + C0DEBEEF0000000000000001 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000002 /* main.m */; }; + C0DEBEEF00000000000000F1 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000101 /* UIKit.framework */; }; + C0DEBEEF00000000000000F2 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000102 /* Foundation.framework */; }; + C0DEBEEF00000000000000F3 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000103 /* WebKit.framework */; }; + C0DEBEEF00000000000000F4 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000104 /* Security.framework */; }; + C0DEBEEF00000000000000F5 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000105 /* CoreFoundation.framework */; }; + C0DEBEEF00000000000000F6 /* libresolv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000106 /* libresolv.tbd */; }; + C0DEBEEF00000000000000F7 /* My Product.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000107 /* My Product.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + C0DEBEEF0000000000000002 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + C0DEBEEF0000000000000003 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + C0DEBEEF0000000000000004 /* My Product.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "My Product.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + C0DEBEEF0000000000000101 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + C0DEBEEF0000000000000102 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + C0DEBEEF0000000000000103 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; + C0DEBEEF0000000000000104 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; + C0DEBEEF0000000000000105 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; }; + C0DEBEEF0000000000000106 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.text-based-dylib-definition; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; }; + C0DEBEEF0000000000000107 /* My Product.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "My Product.a"; path = ../../../bin/My Product.a; sourceTree = SOURCE_ROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXGroup section */ + C0DEBEEF0000000000000010 = { + isa = PBXGroup; + children = ( + C0DEBEEF0000000000000020 /* Products */, + C0DEBEEF0000000000000045 /* Frameworks */, + C0DEBEEF0000000000000030 /* main */, + ); + sourceTree = ""; + }; + C0DEBEEF0000000000000020 /* Products */ = { + isa = PBXGroup; + children = ( + C0DEBEEF0000000000000004 /* My Product.app */, + ); + name = Products; + sourceTree = ""; + }; + C0DEBEEF0000000000000030 /* main */ = { + isa = PBXGroup; + children = ( + C0DEBEEF0000000000000002 /* main.m */, + C0DEBEEF0000000000000003 /* Info.plist */, + ); + path = main; + sourceTree = SOURCE_ROOT; + }; + C0DEBEEF0000000000000045 /* Frameworks */ = { + isa = PBXGroup; + children = ( + C0DEBEEF0000000000000101 /* UIKit.framework */, + C0DEBEEF0000000000000102 /* Foundation.framework */, + C0DEBEEF0000000000000103 /* WebKit.framework */, + C0DEBEEF0000000000000104 /* Security.framework */, + C0DEBEEF0000000000000105 /* CoreFoundation.framework */, + C0DEBEEF0000000000000106 /* libresolv.tbd */, + C0DEBEEF0000000000000107 /* My Product.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + C0DEBEEF0000000000000040 /* My Product */ = { + isa = PBXNativeTarget; + buildConfigurationList = C0DEBEEF0000000000000070 /* Build configuration list for PBXNativeTarget "My Product" */; + buildPhases = ( + C0DEBEEF0000000000000055 /* Prebuild: Wails Go Archive */, + C0DEBEEF0000000000000050 /* Sources */, + C0DEBEEF0000000000000056 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "My Product"; + productName = "My Product"; + productReference = C0DEBEEF0000000000000004 /* My Product.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + C0DEBEEF0000000000000060 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1500; + ORGANIZATIONNAME = "My Company"; + TargetAttributes = { + C0DEBEEF0000000000000040 = { + CreatedOnToolsVersion = 15.0; + }; + }; + }; + buildConfigurationList = C0DEBEEF0000000000000080 /* Build configuration list for PBXProject "main" */; + compatibilityVersion = "Xcode 15.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = C0DEBEEF0000000000000010; + productRefGroup = C0DEBEEF0000000000000020 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + C0DEBEEF0000000000000040 /* My Product */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXFrameworksBuildPhase section */ + C0DEBEEF0000000000000056 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C0DEBEEF00000000000000F7 /* My Product.a in Frameworks */, + C0DEBEEF00000000000000F1 /* UIKit.framework in Frameworks */, + C0DEBEEF00000000000000F2 /* Foundation.framework in Frameworks */, + C0DEBEEF00000000000000F3 /* WebKit.framework in Frameworks */, + C0DEBEEF00000000000000F4 /* Security.framework in Frameworks */, + C0DEBEEF00000000000000F5 /* CoreFoundation.framework in Frameworks */, + C0DEBEEF00000000000000F6 /* libresolv.tbd in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + C0DEBEEF0000000000000055 /* Prebuild: Wails Go Archive */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Prebuild: Wails Go Archive"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "set -e\nAPP_ROOT=\"${PROJECT_DIR}/../../..\"\nSDK_PATH=$(xcrun --sdk iphonesimulator --show-sdk-path)\nexport GOOS=ios\nexport GOARCH=arm64\nexport CGO_ENABLED=1\nexport CGO_CFLAGS=\"-isysroot ${SDK_PATH} -target arm64-apple-ios15.0-simulator -mios-simulator-version-min=15.0\"\nexport CGO_LDFLAGS=\"-isysroot ${SDK_PATH} -target arm64-apple-ios15.0-simulator\"\ncd \"${APP_ROOT}\"\n# Ensure overlay exists\nif [ ! -f build/ios/xcode/overlay.json ]; then\n wails3 ios overlay:gen -out build/ios/xcode/overlay.json -config build/config.yml || true\nfi\n# Build Go c-archive if missing or older than sources\nif [ ! -f bin/My Product.a ]; then\n echo \"Building Go c-archive...\"\n go build -buildmode=c-archive -overlay build/ios/xcode/overlay.json -o bin/My Product.a\nfi\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + C0DEBEEF0000000000000050 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C0DEBEEF0000000000000001 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + C0DEBEEF0000000000000090 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = main/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.updaterexample"; + PRODUCT_NAME = "My Product"; + CODE_SIGNING_ALLOWED = NO; + SDKROOT = iphonesimulator; + }; + name = Debug; + }; + C0DEBEEF00000000000000A0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = main/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.updaterexample"; + PRODUCT_NAME = "My Product"; + CODE_SIGNING_ALLOWED = NO; + SDKROOT = iphonesimulator; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + C0DEBEEF0000000000000070 /* Build configuration list for PBXNativeTarget "My Product" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C0DEBEEF0000000000000090 /* Debug */, + C0DEBEEF00000000000000A0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; + C0DEBEEF0000000000000080 /* Build configuration list for PBXProject "main" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C0DEBEEF0000000000000090 /* Debug */, + C0DEBEEF00000000000000A0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; +/* End XCConfigurationList section */ + }; + rootObject = C0DEBEEF0000000000000060 /* Project object */; +} diff --git a/examples/wails3_init_updater/build/ios/scripts/deps/install_deps.go b/examples/wails3_init_updater/build/ios/scripts/deps/install_deps.go new file mode 100644 index 0000000..88ed47a --- /dev/null +++ b/examples/wails3_init_updater/build/ios/scripts/deps/install_deps.go @@ -0,0 +1,319 @@ +// install_deps.go - iOS development dependency checker +// This script checks for required iOS development tools. +// It's designed to be portable across different shells by using Go instead of shell scripts. +// +// Usage: +// go run install_deps.go # Interactive mode +// TASK_FORCE_YES=true go run install_deps.go # Auto-accept prompts +// CI=true go run install_deps.go # CI mode (auto-accept) + +package main + +import ( + "bufio" + "fmt" + "os" + "os/exec" + "strings" +) + +type Dependency struct { + Name string + CheckFunc func() (bool, string) // Returns (success, details) + Required bool + InstallCmd []string + InstallMsg string + SuccessMsg string + FailureMsg string +} + +func main() { + fmt.Println("Checking iOS development dependencies...") + fmt.Println("=" + strings.Repeat("=", 50)) + fmt.Println() + + hasErrors := false + dependencies := []Dependency{ + { + Name: "Xcode", + CheckFunc: func() (bool, string) { + // Check if xcodebuild exists + if !checkCommand([]string{"xcodebuild", "-version"}) { + return false, "" + } + // Get version info + out, err := exec.Command("xcodebuild", "-version").Output() + if err != nil { + return false, "" + } + lines := strings.Split(string(out), "\n") + if len(lines) > 0 { + return true, strings.TrimSpace(lines[0]) + } + return true, "" + }, + Required: true, + InstallMsg: "Please install Xcode from the Mac App Store:\n https://apps.apple.com/app/xcode/id497799835\n Xcode is REQUIRED for iOS development (includes iOS SDKs, simulators, and frameworks)", + SuccessMsg: "✅ Xcode found", + FailureMsg: "❌ Xcode not found (REQUIRED)", + }, + { + Name: "Xcode Developer Path", + CheckFunc: func() (bool, string) { + // Check if xcode-select points to a valid Xcode path + out, err := exec.Command("xcode-select", "-p").Output() + if err != nil { + return false, "xcode-select not configured" + } + path := strings.TrimSpace(string(out)) + + // Check if path exists and is in Xcode.app + if _, err := os.Stat(path); err != nil { + return false, "Invalid Xcode path" + } + + // Verify it's pointing to Xcode.app (not just Command Line Tools) + if !strings.Contains(path, "Xcode.app") { + return false, fmt.Sprintf("Points to %s (should be Xcode.app)", path) + } + + return true, path + }, + Required: true, + InstallCmd: []string{"sudo", "xcode-select", "-s", "/Applications/Xcode.app/Contents/Developer"}, + InstallMsg: "Xcode developer path needs to be configured", + SuccessMsg: "✅ Xcode developer path configured", + FailureMsg: "❌ Xcode developer path not configured correctly", + }, + { + Name: "iOS SDK", + CheckFunc: func() (bool, string) { + // Get the iOS Simulator SDK path + cmd := exec.Command("xcrun", "--sdk", "iphonesimulator", "--show-sdk-path") + output, err := cmd.Output() + if err != nil { + return false, "Cannot find iOS SDK" + } + sdkPath := strings.TrimSpace(string(output)) + + // Check if the SDK path exists + if _, err := os.Stat(sdkPath); err != nil { + return false, "iOS SDK path not found" + } + + // Check for UIKit framework (essential for iOS development) + uikitPath := fmt.Sprintf("%s/System/Library/Frameworks/UIKit.framework", sdkPath) + if _, err := os.Stat(uikitPath); err != nil { + return false, "UIKit.framework not found" + } + + // Get SDK version + versionCmd := exec.Command("xcrun", "--sdk", "iphonesimulator", "--show-sdk-version") + versionOut, _ := versionCmd.Output() + version := strings.TrimSpace(string(versionOut)) + + return true, fmt.Sprintf("iOS %s SDK", version) + }, + Required: true, + InstallMsg: "iOS SDK comes with Xcode. Please ensure Xcode is properly installed.", + SuccessMsg: "✅ iOS SDK found with UIKit framework", + FailureMsg: "❌ iOS SDK not found or incomplete", + }, + { + Name: "iOS Simulator Runtime", + CheckFunc: func() (bool, string) { + if !checkCommand([]string{"xcrun", "simctl", "help"}) { + return false, "" + } + // Check if we can list runtimes + out, err := exec.Command("xcrun", "simctl", "list", "runtimes").Output() + if err != nil { + return false, "Cannot access simulator" + } + // Count iOS runtimes + lines := strings.Split(string(out), "\n") + count := 0 + var versions []string + for _, line := range lines { + if strings.Contains(line, "iOS") && !strings.Contains(line, "unavailable") { + count++ + // Extract version number + if parts := strings.Fields(line); len(parts) > 2 { + for _, part := range parts { + if strings.HasPrefix(part, "(") && strings.HasSuffix(part, ")") { + versions = append(versions, strings.Trim(part, "()")) + break + } + } + } + } + } + if count > 0 { + return true, fmt.Sprintf("%d runtime(s): %s", count, strings.Join(versions, ", ")) + } + return false, "No iOS runtimes installed" + }, + Required: true, + InstallMsg: "iOS Simulator runtimes come with Xcode. You may need to download them:\n Xcode → Settings → Platforms → iOS", + SuccessMsg: "✅ iOS Simulator runtime available", + FailureMsg: "❌ iOS Simulator runtime not available", + }, + } + + // Check each dependency + for _, dep := range dependencies { + success, details := dep.CheckFunc() + if success { + msg := dep.SuccessMsg + if details != "" { + msg = fmt.Sprintf("%s (%s)", dep.SuccessMsg, details) + } + fmt.Println(msg) + } else { + fmt.Println(dep.FailureMsg) + if details != "" { + fmt.Printf(" Details: %s\n", details) + } + if dep.Required { + hasErrors = true + if len(dep.InstallCmd) > 0 { + fmt.Println() + fmt.Println(" " + dep.InstallMsg) + fmt.Printf(" Fix command: %s\n", strings.Join(dep.InstallCmd, " ")) + if promptUser("Do you want to run this command?") { + fmt.Println("Running command...") + cmd := exec.Command(dep.InstallCmd[0], dep.InstallCmd[1:]...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + if err := cmd.Run(); err != nil { + fmt.Printf("Command failed: %v\n", err) + os.Exit(1) + } + fmt.Println("✅ Command completed. Please run this check again.") + } else { + fmt.Printf(" Please run manually: %s\n", strings.Join(dep.InstallCmd, " ")) + } + } else { + fmt.Println(" " + dep.InstallMsg) + } + } + } + } + + // Check for iPhone simulators + fmt.Println() + fmt.Println("Checking for iPhone simulator devices...") + if !checkCommand([]string{"xcrun", "simctl", "list", "devices"}) { + fmt.Println("❌ Cannot check for iPhone simulators") + hasErrors = true + } else { + out, err := exec.Command("xcrun", "simctl", "list", "devices").Output() + if err != nil { + fmt.Println("❌ Failed to list simulator devices") + hasErrors = true + } else if !strings.Contains(string(out), "iPhone") { + fmt.Println("⚠️ No iPhone simulator devices found") + fmt.Println() + + // Get the latest iOS runtime + runtimeOut, err := exec.Command("xcrun", "simctl", "list", "runtimes").Output() + if err != nil { + fmt.Println(" Failed to get iOS runtimes:", err) + } else { + lines := strings.Split(string(runtimeOut), "\n") + var latestRuntime string + for _, line := range lines { + if strings.Contains(line, "iOS") && !strings.Contains(line, "unavailable") { + // Extract runtime identifier + parts := strings.Fields(line) + if len(parts) > 0 { + latestRuntime = parts[len(parts)-1] + } + } + } + + if latestRuntime == "" { + fmt.Println(" No iOS runtime found. Please install iOS simulators in Xcode:") + fmt.Println(" Xcode → Settings → Platforms → iOS") + } else { + fmt.Println(" Would you like to create an iPhone 15 Pro simulator?") + createCmd := []string{"xcrun", "simctl", "create", "iPhone 15 Pro", "iPhone 15 Pro", latestRuntime} + fmt.Printf(" Command: %s\n", strings.Join(createCmd, " ")) + if promptUser("Create simulator?") { + cmd := exec.Command(createCmd[0], createCmd[1:]...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + fmt.Printf(" Failed to create simulator: %v\n", err) + } else { + fmt.Println(" ✅ iPhone 15 Pro simulator created") + } + } else { + fmt.Println(" Skipping simulator creation") + fmt.Printf(" Create manually: %s\n", strings.Join(createCmd, " ")) + } + } + } + } else { + // Count iPhone devices + count := 0 + lines := strings.Split(string(out), "\n") + for _, line := range lines { + if strings.Contains(line, "iPhone") && !strings.Contains(line, "unavailable") { + count++ + } + } + fmt.Printf("✅ %d iPhone simulator device(s) available\n", count) + } + } + + // Final summary + fmt.Println() + fmt.Println("=" + strings.Repeat("=", 50)) + if hasErrors { + fmt.Println("❌ Some required dependencies are missing or misconfigured.") + fmt.Println() + fmt.Println("Quick setup guide:") + fmt.Println("1. Install Xcode from Mac App Store (if not installed)") + fmt.Println("2. Open Xcode once and agree to the license") + fmt.Println("3. Install additional components when prompted") + fmt.Println("4. Run: sudo xcode-select -s /Applications/Xcode.app/Contents/Developer") + fmt.Println("5. Download iOS simulators: Xcode → Settings → Platforms → iOS") + fmt.Println("6. Run this check again") + os.Exit(1) + } else { + fmt.Println("✅ All required dependencies are installed!") + fmt.Println(" You're ready for iOS development with Wails!") + } +} + +func checkCommand(args []string) bool { + if len(args) == 0 { + return false + } + cmd := exec.Command(args[0], args[1:]...) + cmd.Stdout = nil + cmd.Stderr = nil + err := cmd.Run() + return err == nil +} + +func promptUser(question string) bool { + // Check if we're in a non-interactive environment + if os.Getenv("CI") != "" || os.Getenv("TASK_FORCE_YES") == "true" { + fmt.Printf("%s [y/N]: y (auto-accepted)\n", question) + return true + } + + reader := bufio.NewReader(os.Stdin) + fmt.Printf("%s [y/N]: ", question) + + response, err := reader.ReadString('\n') + if err != nil { + return false + } + + response = strings.ToLower(strings.TrimSpace(response)) + return response == "y" || response == "yes" +} \ No newline at end of file diff --git a/examples/wails3_init_updater/build/linux/Taskfile.yml b/examples/wails3_init_updater/build/linux/Taskfile.yml new file mode 100644 index 0000000..19ba899 --- /dev/null +++ b/examples/wails3_init_updater/build/linux/Taskfile.yml @@ -0,0 +1,226 @@ +version: "3" + +includes: + common: ../Taskfile.yml + +vars: + # Signing configuration - edit these values for your project + # PGP_KEY: "path/to/signing-key.asc" + # SIGN_ROLE: "builder" # Options: origin, maint, archive, builder + # + # Password is stored securely in system keychain. Run: wails3 setup signing + + # Docker image for cross-compilation (used when building on non-Linux or no CC available) + CROSS_IMAGE: wails-cross + +tasks: + build: + summary: Builds the application for Linux + cmds: + # Linux requires CGO - use Docker when: + # 1. Cross-compiling from non-Linux, OR + # 2. No C compiler is available, OR + # 3. Target architecture differs from host architecture (cross-arch compilation) + - task: '{{if and (eq OS "linux") (eq .HAS_CC "true") (eq .TARGET_ARCH ARCH)}}build:native{{else}}build:docker{{end}}' + vars: + ARCH: "{{.ARCH}}" + DEV: "{{.DEV}}" + OUTPUT: "{{.OUTPUT}}" + EXTRA_TAGS: "{{.EXTRA_TAGS}}" + vars: + DEFAULT_OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}" + OUTPUT: "{{ .OUTPUT | default .DEFAULT_OUTPUT }}" + # Determine target architecture (defaults to host ARCH if not specified) + TARGET_ARCH: "{{.ARCH | default ARCH}}" + # Check if a C compiler is available (gcc or clang) + HAS_CC: + sh: '(command -v gcc >/dev/null 2>&1 || command -v clang >/dev/null 2>&1) && echo "true" || echo "false"' + + build:native: + summary: Builds the application natively on Linux + internal: true + deps: + - task: common:go:mod:tidy + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + DEV: + ref: .DEV + - task: common:generate:icons + - task: generate:dotdesktop + cmds: + - go build {{.BUILD_FLAGS}} -o {{.OUTPUT}} + vars: + BUILD_FLAGS: '{{if eq .DEV "true"}}{{if .EXTRA_TAGS}}-tags {{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s"{{end}}' + DEFAULT_OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}" + OUTPUT: "{{ .OUTPUT | default .DEFAULT_OUTPUT }}" + env: + GOOS: linux + CGO_ENABLED: 1 + GOARCH: "{{.ARCH | default ARCH}}" + + build:docker: + summary: Builds for Linux using Docker (for non-Linux hosts or when no C compiler available) + internal: true + deps: + - task: common:build:frontend + - task: common:generate:icons + - task: generate:dotdesktop + preconditions: + - sh: docker info > /dev/null 2>&1 + msg: "Docker is required for cross-compilation to Linux. Please install Docker." + - sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1 + msg: | + Docker image '{{.CROSS_IMAGE}}' not found. + Build it first: wails3 task setup:docker + cmds: + - docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{if .EXTRA_TAGS}}-e EXTRA_TAGS="{{.EXTRA_TAGS}}"{{end}} "{{.CROSS_IMAGE}}" linux {{.DOCKER_ARCH}} + - docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin + - mkdir -p {{.BIN_DIR}} + - mv "bin/{{.APP_NAME}}-linux-{{.DOCKER_ARCH}}" "{{.OUTPUT}}" + vars: + DOCKER_ARCH: '{{.ARCH | default "amd64"}}' + DEFAULT_OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}" + OUTPUT: "{{ .OUTPUT | default .DEFAULT_OUTPUT }}" + # Mount Go module cache for faster builds + GO_CACHE_MOUNT: + sh: 'echo "-v ${GOPATH:-$HOME/go}/pkg/mod:/go/pkg/mod"' + # Extract replace directives from go.mod and create -v mounts for each + REPLACE_MOUNTS: + sh: | + grep -E '^replace .* => ' go.mod 2>/dev/null | while read -r line; do + path=$(echo "$line" | sed -E 's/^replace .* => //' | tr -d '\r') + # Convert relative paths to absolute + if [ "${path#/}" = "$path" ]; then + path="$(cd "$(dirname "$path")" 2>/dev/null && pwd)/$(basename "$path")" + fi + # Only mount if directory exists + if [ -d "$path" ]; then + echo "-v $path:$path:ro" + fi + done | tr '\n' ' ' + + package: + summary: Packages the application for Linux + deps: + - task: build + cmds: + - task: create:appimage + - task: create:deb + - task: create:rpm + - task: create:aur + + create:appimage: + summary: Creates an AppImage + dir: build/linux/appimage + deps: + - task: build + - task: generate:dotdesktop + cmds: + - cp "{{.APP_BINARY}}" "{{.APP_NAME}}" + - cp ../../appicon.png "{{.APP_NAME}}.png" + - wails3 generate appimage -binary "{{.APP_NAME}}" -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/linux/appimage/build + vars: + APP_NAME: "{{.APP_NAME}}" + APP_BINARY: "../../../bin/{{.APP_NAME}}" + ICON: "{{.APP_NAME}}.png" + DESKTOP_FILE: "../{{.APP_NAME}}.desktop" + OUTPUT_DIR: "../../../bin" + + create:deb: + summary: Creates a deb package + deps: + - task: build + cmds: + - task: generate:dotdesktop + - task: generate:deb + + create:rpm: + summary: Creates a rpm package + deps: + - task: build + cmds: + - task: generate:dotdesktop + - task: generate:rpm + + create:aur: + summary: Creates a arch linux packager package + deps: + - task: build + cmds: + - task: generate:dotdesktop + - task: generate:aur + + generate:deb: + summary: Creates a deb package + cmds: + - wails3 tool package -name "{{.APP_NAME}}" -format deb -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin + + generate:rpm: + summary: Creates a rpm package + cmds: + - wails3 tool package -name "{{.APP_NAME}}" -format rpm -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin + + generate:aur: + summary: Creates a arch linux packager package + cmds: + - wails3 tool package -name "{{.APP_NAME}}" -format archlinux -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin + + generate:dotdesktop: + summary: Generates a `.desktop` file + dir: build + cmds: + - mkdir -p {{.ROOT_DIR}}/build/linux/appimage + - wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile "{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop" -categories "{{.CATEGORIES}}" + vars: + APP_NAME: "{{.APP_NAME}}" + EXEC: "{{.APP_NAME}}" + ICON: "{{.APP_NAME}}" + CATEGORIES: "Development;" + OUTPUTFILE: "{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop" + + run: + cmds: + - "{{.BIN_DIR}}/{{.APP_NAME}}" + + sign:deb: + summary: Signs the DEB package + desc: | + Signs the .deb package with a PGP key. + Configure PGP_KEY in the vars section at the top of this file. + Password is retrieved from system keychain (run: wails3 setup signing) + deps: + - task: create:deb + cmds: + - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}*.deb" --pgp-key {{.PGP_KEY}} {{if .SIGN_ROLE}}--role {{.SIGN_ROLE}}{{end}} + preconditions: + - sh: '[ -n "{{.PGP_KEY}}" ]' + msg: "PGP_KEY is required. Set it in the vars section at the top of build/linux/Taskfile.yml" + + sign:rpm: + summary: Signs the RPM package + desc: | + Signs the .rpm package with a PGP key. + Configure PGP_KEY in the vars section at the top of this file. + Password is retrieved from system keychain (run: wails3 setup signing) + deps: + - task: create:rpm + cmds: + - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}*.rpm" --pgp-key {{.PGP_KEY}} + preconditions: + - sh: '[ -n "{{.PGP_KEY}}" ]' + msg: "PGP_KEY is required. Set it in the vars section at the top of build/linux/Taskfile.yml" + + sign:packages: + summary: Signs all Linux packages (DEB and RPM) + desc: | + Signs both .deb and .rpm packages with a PGP key. + Configure PGP_KEY in the vars section at the top of this file. + Password is retrieved from system keychain (run: wails3 setup signing) + cmds: + - task: sign:deb + - task: sign:rpm + preconditions: + - sh: '[ -n "{{.PGP_KEY}}" ]' + msg: "PGP_KEY is required. Set it in the vars section at the top of build/linux/Taskfile.yml" diff --git a/examples/wails3_init_updater/build/linux/appimage/build.sh b/examples/wails3_init_updater/build/linux/appimage/build.sh new file mode 100644 index 0000000..8f9d6d4 --- /dev/null +++ b/examples/wails3_init_updater/build/linux/appimage/build.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Copyright (c) 2018-Present Lea Anthony +# SPDX-License-Identifier: MIT + +# Fail script on any error +set -euxo pipefail + +# Define variables +APP_DIR="${APP_NAME}.AppDir" + +# Create AppDir structure +mkdir -p "${APP_DIR}/usr/bin" +cp -r "${APP_BINARY}" "${APP_DIR}/usr/bin/" +cp "${ICON_PATH}" "${APP_DIR}/" +cp "${DESKTOP_FILE}" "${APP_DIR}/" + +if [[ $(uname -m) == *x86_64* ]]; then + # Download linuxdeploy and make it executable + wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage + chmod +x linuxdeploy-x86_64.AppImage + + # Run linuxdeploy to bundle the application + ./linuxdeploy-x86_64.AppImage --appdir "${APP_DIR}" --output appimage +else + # Download linuxdeploy and make it executable (arm64) + wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-aarch64.AppImage + chmod +x linuxdeploy-aarch64.AppImage + + # Run linuxdeploy to bundle the application (arm64) + ./linuxdeploy-aarch64.AppImage --appdir "${APP_DIR}" --output appimage +fi + +# Rename the generated AppImage +mv "${APP_NAME}*.AppImage" "${APP_NAME}.AppImage" diff --git a/examples/wails3_init_updater/build/linux/desktop b/examples/wails3_init_updater/build/linux/desktop new file mode 100644 index 0000000..7a71802 --- /dev/null +++ b/examples/wails3_init_updater/build/linux/desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Version=1.0 +Name=My Product +Comment=A updater-example application +# The Exec line includes %u to pass the URL to the application +Exec=/usr/local/bin/updater-example %u +Terminal=false +Type=Application +Icon=updater-example +Categories=Utility; +StartupWMClass=updater-example + + diff --git a/examples/wails3_init_updater/build/linux/nfpm/nfpm.yaml b/examples/wails3_init_updater/build/linux/nfpm/nfpm.yaml new file mode 100644 index 0000000..3ef809f --- /dev/null +++ b/examples/wails3_init_updater/build/linux/nfpm/nfpm.yaml @@ -0,0 +1,67 @@ +# Feel free to remove those if you don't want/need to use them. +# Make sure to check the documentation at https://nfpm.goreleaser.com +# +# The lines below are called `modelines`. See `:help modeline` + +name: "updater-example" +arch: ${GOARCH} +platform: "linux" +version: "0.1.0" +section: "default" +priority: "extra" +maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}> +description: "A updater-example application" +vendor: "My Company" +homepage: "https://wails.io" +license: "MIT" +release: "1" + +contents: + - src: "./bin/updater-example" + dst: "/usr/local/bin/updater-example" + - src: "./build/appicon.png" + dst: "/usr/share/icons/hicolor/128x128/apps/updater-example.png" + - src: "./build/linux/updater-example.desktop" + dst: "/usr/share/applications/updater-example.desktop" + +# Default dependencies for Debian 12/Ubuntu 22.04+ with WebKit 4.1 +depends: + - libgtk-3-0 + - libwebkit2gtk-4.1-0 + +# Distribution-specific overrides for different package formats and WebKit versions +overrides: + # RPM packages for RHEL/CentOS/AlmaLinux/Rocky Linux (WebKit 4.0) + rpm: + depends: + - gtk3 + - webkit2gtk4.1 + + # Arch Linux packages (WebKit 4.1) + archlinux: + depends: + - gtk3 + - webkit2gtk-4.1 + +# scripts section to ensure desktop database is updated after install +scripts: + postinstall: "./build/linux/nfpm/scripts/postinstall.sh" + # You can also add preremove, postremove if needed + # preremove: "./build/linux/nfpm/scripts/preremove.sh" + # postremove: "./build/linux/nfpm/scripts/postremove.sh" + +# replaces: +# - foobar +# provides: +# - bar +# depends: +# - gtk3 +# - libwebkit2gtk +# recommends: +# - whatever +# suggests: +# - something-else +# conflicts: +# - not-foo +# - not-bar +# changelog: "changelog.yaml" diff --git a/examples/wails3_init_updater/build/linux/nfpm/scripts/postinstall.sh b/examples/wails3_init_updater/build/linux/nfpm/scripts/postinstall.sh new file mode 100644 index 0000000..4bbb815 --- /dev/null +++ b/examples/wails3_init_updater/build/linux/nfpm/scripts/postinstall.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# Update desktop database for .desktop file changes +# This makes the application appear in application menus and registers its capabilities. +if command -v update-desktop-database >/dev/null 2>&1; then + echo "Updating desktop database..." + update-desktop-database -q /usr/share/applications +else + echo "Warning: update-desktop-database command not found. Desktop file may not be immediately recognized." >&2 +fi + +# Update MIME database for custom URL schemes (x-scheme-handler) +# This ensures the system knows how to handle your custom protocols. +if command -v update-mime-database >/dev/null 2>&1; then + echo "Updating MIME database..." + update-mime-database -n /usr/share/mime +else + echo "Warning: update-mime-database command not found. Custom URL schemes may not be immediately recognized." >&2 +fi + +exit 0 diff --git a/examples/wails3_init_updater/build/linux/nfpm/scripts/postremove.sh b/examples/wails3_init_updater/build/linux/nfpm/scripts/postremove.sh new file mode 100644 index 0000000..a9bf588 --- /dev/null +++ b/examples/wails3_init_updater/build/linux/nfpm/scripts/postremove.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/examples/wails3_init_updater/build/linux/nfpm/scripts/preinstall.sh b/examples/wails3_init_updater/build/linux/nfpm/scripts/preinstall.sh new file mode 100644 index 0000000..a9bf588 --- /dev/null +++ b/examples/wails3_init_updater/build/linux/nfpm/scripts/preinstall.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/examples/wails3_init_updater/build/linux/nfpm/scripts/preremove.sh b/examples/wails3_init_updater/build/linux/nfpm/scripts/preremove.sh new file mode 100644 index 0000000..a9bf588 --- /dev/null +++ b/examples/wails3_init_updater/build/linux/nfpm/scripts/preremove.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/examples/wails3_init_updater/build/windows/Taskfile.yml b/examples/wails3_init_updater/build/windows/Taskfile.yml new file mode 100644 index 0000000..356d71c --- /dev/null +++ b/examples/wails3_init_updater/build/windows/Taskfile.yml @@ -0,0 +1,184 @@ +version: "3" + +includes: + common: ../Taskfile.yml + +vars: + # Signing configuration - edit these values for your project + # SIGN_CERTIFICATE: "path/to/certificate.pfx" + # SIGN_THUMBPRINT: "certificate-thumbprint" # Alternative to SIGN_CERTIFICATE + # TIMESTAMP_SERVER: "http://timestamp.digicert.com" + # + # Password is stored securely in system keychain. Run: wails3 setup signing + + # Docker image for cross-compilation with CGO (used when CGO_ENABLED=1 on non-Windows) + CROSS_IMAGE: wails-cross + +tasks: + build: + summary: Builds the application for Windows + cmds: + # Auto-detect CGO: if CGO_ENABLED=1, use Docker; otherwise use native Go cross-compile + - task: '{{if and (ne OS "windows") (eq .CGO_ENABLED "1")}}build:docker{{else}}build:native{{end}}' + vars: + ARCH: "{{.ARCH}}" + DEV: "{{.DEV}}" + EXTRA_TAGS: "{{.EXTRA_TAGS}}" + vars: + # Default to CGO_ENABLED=0 if not explicitly set + CGO_ENABLED: '{{.CGO_ENABLED | default "0"}}' + + build:native: + summary: Builds the application using native Go cross-compilation + internal: true + deps: + - task: common:go:mod:tidy + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + DEV: + ref: .DEV + - task: common:generate:icons + cmds: + - task: generate:syso + - go build {{.BUILD_FLAGS}} -o "{{.BIN_DIR}}/{{.APP_NAME}}.exe" + - cmd: powershell Remove-item *.syso + platforms: [windows] + - cmd: rm -f *.syso + platforms: [linux, darwin] + vars: + BUILD_FLAGS: '{{if eq .DEV "true"}}{{if .EXTRA_TAGS}}-tags {{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{end}}' + env: + GOOS: windows + CGO_ENABLED: '{{.CGO_ENABLED | default "0"}}' + GOARCH: "{{.ARCH | default ARCH}}" + + build:docker: + summary: Cross-compiles for Windows using Docker with Zig (for CGO builds on non-Windows) + internal: true + deps: + - task: common:build:frontend + - task: common:generate:icons + preconditions: + - sh: docker info > /dev/null 2>&1 + msg: "Docker is required for CGO cross-compilation. Please install Docker." + - sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1 + msg: | + Docker image '{{.CROSS_IMAGE}}' not found. + Build it first: wails3 task setup:docker + cmds: + - task: generate:syso + - docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{if .EXTRA_TAGS}}-e EXTRA_TAGS="{{.EXTRA_TAGS}}"{{end}} {{.CROSS_IMAGE}} windows {{.DOCKER_ARCH}} + - docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin + - rm -f *.syso + vars: + DOCKER_ARCH: '{{.ARCH | default "amd64"}}' + # Mount Go module cache for faster builds + GO_CACHE_MOUNT: + sh: 'echo "-v ${GOPATH:-$HOME/go}/pkg/mod:/go/pkg/mod"' + # Extract replace directives from go.mod and create -v mounts for each + REPLACE_MOUNTS: + sh: | + grep -E '^replace .* => ' go.mod 2>/dev/null | while read -r line; do + path=$(echo "$line" | sed -E 's/^replace .* => //' | tr -d '\r') + # Convert relative paths to absolute + if [ "${path#/}" = "$path" ]; then + path="$(cd "$(dirname "$path")" 2>/dev/null && pwd)/$(basename "$path")" + fi + # Only mount if directory exists + if [ -d "$path" ]; then + echo "-v $path:$path:ro" + fi + done | tr '\n' ' ' + + package: + summary: Packages the application + cmds: + - task: '{{if eq (.FORMAT | default "nsis") "msix"}}create:msix:package{{else}}create:nsis:installer{{end}}' + vars: + FORMAT: '{{.FORMAT | default "nsis"}}' + + generate:syso: + summary: Generates Windows `.syso` file + dir: build + cmds: + - wails3 generate syso -arch {{.ARCH}} -icon windows/icon.ico -manifest windows/wails.exe.manifest -info windows/info.json -out ../wails_windows_{{.ARCH}}.syso + vars: + ARCH: "{{.ARCH | default ARCH}}" + + create:nsis:installer: + summary: Creates an NSIS installer + dir: build/windows/nsis + deps: + - task: build + cmds: + # Create the Microsoft WebView2 bootstrapper if it doesn't exist + - wails3 generate webview2bootstrapper -dir "{{.ROOT_DIR}}/build/windows/nsis" + - | + {{if eq OS "windows"}} + makensis -DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}\{{.BIN_DIR}}\{{.APP_NAME}}.exe" project.nsi + {{else}} + makensis -DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" project.nsi + {{end}} + vars: + ARCH: "{{.ARCH | default ARCH}}" + ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}' + + create:msix:package: + summary: Creates an MSIX package + deps: + - task: build + cmds: + - |- + wails3 tool msix \ + --config "{{.ROOT_DIR}}/wails.json" \ + --name "{{.APP_NAME}}" \ + --executable "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" \ + --arch "{{.ARCH}}" \ + --out "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}-{{.ARCH}}.msix" \ + {{if .CERT_PATH}}--cert "{{.CERT_PATH}}"{{end}} \ + {{if .PUBLISHER}}--publisher "{{.PUBLISHER}}"{{end}} \ + {{if .USE_MSIX_TOOL}}--use-msix-tool{{else}}--use-makeappx{{end}} + vars: + ARCH: "{{.ARCH | default ARCH}}" + CERT_PATH: '{{.CERT_PATH | default ""}}' + PUBLISHER: '{{.PUBLISHER | default ""}}' + USE_MSIX_TOOL: '{{.USE_MSIX_TOOL | default "false"}}' + + install:msix:tools: + summary: Installs tools required for MSIX packaging + cmds: + - wails3 tool msix-install-tools + + run: + cmds: + - "{{.BIN_DIR}}/{{.APP_NAME}}.exe" + + sign: + summary: Signs the Windows executable + desc: | + Signs the .exe with an Authenticode certificate. + Configure SIGN_CERTIFICATE or SIGN_THUMBPRINT in the vars section at the top of this file. + Password is retrieved from system keychain (run: wails3 setup signing) + deps: + - task: build + cmds: + - wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.exe" {{if .SIGN_CERTIFICATE}}--certificate {{.SIGN_CERTIFICATE}}{{end}} {{if .SIGN_THUMBPRINT}}--thumbprint {{.SIGN_THUMBPRINT}}{{end}} {{if .TIMESTAMP_SERVER}}--timestamp {{.TIMESTAMP_SERVER}}{{end}} + preconditions: + - sh: '[ -n "{{.SIGN_CERTIFICATE}}" ] || [ -n "{{.SIGN_THUMBPRINT}}" ]' + msg: "Either SIGN_CERTIFICATE or SIGN_THUMBPRINT is required. Set it in the vars section at the top of build/windows/Taskfile.yml" + + sign:installer: + summary: Signs the NSIS installer + desc: | + Creates and signs the NSIS installer. + Configure SIGN_CERTIFICATE or SIGN_THUMBPRINT in the vars section at the top of this file. + Password is retrieved from system keychain (run: wails3 setup signing) + deps: + - task: create:nsis:installer + cmds: + - wails3 tool sign --input "build/windows/nsis/{{.APP_NAME}}-installer.exe" {{if .SIGN_CERTIFICATE}}--certificate {{.SIGN_CERTIFICATE}}{{end}} {{if .SIGN_THUMBPRINT}}--thumbprint {{.SIGN_THUMBPRINT}}{{end}} {{if .TIMESTAMP_SERVER}}--timestamp {{.TIMESTAMP_SERVER}}{{end}} + preconditions: + - sh: '[ -n "{{.SIGN_CERTIFICATE}}" ] || [ -n "{{.SIGN_THUMBPRINT}}" ]' + msg: "Either SIGN_CERTIFICATE or SIGN_THUMBPRINT is required. Set it in the vars section at the top of build/windows/Taskfile.yml" diff --git a/examples/wails3_init_updater/build/windows/icon.ico b/examples/wails3_init_updater/build/windows/icon.ico new file mode 100644 index 0000000..bfa0690 Binary files /dev/null and b/examples/wails3_init_updater/build/windows/icon.ico differ diff --git a/examples/wails3_init_updater/build/windows/info.json b/examples/wails3_init_updater/build/windows/info.json new file mode 100644 index 0000000..f719b7e --- /dev/null +++ b/examples/wails3_init_updater/build/windows/info.json @@ -0,0 +1,15 @@ +{ + "fixed": { + "file_version": "0.1.0" + }, + "info": { + "0000": { + "ProductVersion": "0.1.0", + "CompanyName": "My Company", + "FileDescription": "A updater-example application", + "LegalCopyright": "© 2026, My Company", + "ProductName": "My Product", + "Comments": "This is a comment" + } + } +} diff --git a/examples/wails3_init_updater/build/windows/msix/app_manifest.xml b/examples/wails3_init_updater/build/windows/msix/app_manifest.xml new file mode 100644 index 0000000..539d07f --- /dev/null +++ b/examples/wails3_init_updater/build/windows/msix/app_manifest.xml @@ -0,0 +1,55 @@ + + + + + + + My Product + My Company + A updater-example application + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/wails3_init_updater/build/windows/msix/template.xml b/examples/wails3_init_updater/build/windows/msix/template.xml new file mode 100644 index 0000000..e99aa7a --- /dev/null +++ b/examples/wails3_init_updater/build/windows/msix/template.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + false + My Product + My Company + A updater-example application + Assets\AppIcon.png + + + + + + + diff --git a/examples/wails3_init_updater/build/windows/nsis/project.nsi b/examples/wails3_init_updater/build/windows/nsis/project.nsi new file mode 100644 index 0000000..285aa5d --- /dev/null +++ b/examples/wails3_init_updater/build/windows/nsis/project.nsi @@ -0,0 +1,114 @@ +Unicode true + +#### +## Please note: Template replacements don't work in this file. They are provided with default defines like +## mentioned underneath. +## If the keyword is not defined, "wails_tools.nsh" will populate them. +## If they are defined here, "wails_tools.nsh" will not touch them. This allows you to use this project.nsi manually +## from outside of Wails for debugging and development of the installer. +## +## For development first make a wails nsis build to populate the "wails_tools.nsh": +## > wails build --target windows/amd64 --nsis +## Then you can call makensis on this file with specifying the path to your binary: +## For a AMD64 only installer: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe +## For a ARM64 only installer: +## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe +## For a installer with both architectures: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe +#### +## The following information is taken from the wails_tools.nsh file, but they can be overwritten here. +#### +## !define INFO_PROJECTNAME "my-project" # Default "updater-example" +## !define INFO_COMPANYNAME "My Company" # Default "My Company" +## !define INFO_PRODUCTNAME "My Product Name" # Default "My Product" +## !define INFO_PRODUCTVERSION "1.0.0" # Default "0.1.0" +## !define INFO_COPYRIGHT "(c) Now, My Company" # Default "© 2026, My Company" +### +## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe" +## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +#### +## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html +#### +## Include the wails tools +#### +!include "wails_tools.nsh" + +# The version information for this two must consist of 4 parts +VIProductVersion "${INFO_PRODUCTVERSION}.0" +VIFileVersion "${INFO_PRODUCTVERSION}.0" + +VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}" +VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer" +VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}" +VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}" + +# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware +ManifestDPIAware true + +!include "MUI.nsh" + +!define MUI_ICON "..\icon.ico" +!define MUI_UNICON "..\icon.ico" +# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314 +!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps +!define MUI_ABORTWARNING # This will warn the user if they exit from the installer. + +!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page. +# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer +!insertmacro MUI_PAGE_DIRECTORY # In which folder install page. +!insertmacro MUI_PAGE_INSTFILES # Installing page. +!insertmacro MUI_PAGE_FINISH # Finished installation page. + +!insertmacro MUI_UNPAGE_INSTFILES # Uninstalling page + +!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer + +## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1 +#!uninstfinalize 'signtool --file "%1"' +#!finalize 'signtool --file "%1"' + +Name "${INFO_PRODUCTNAME}" +OutFile "..\..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file. +InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder). +ShowInstDetails show # This will always show the installation details. + +Function .onInit + !insertmacro wails.checkArchitecture +FunctionEnd + +Section + !insertmacro wails.setShellContext + + !insertmacro wails.webview2runtime + + SetOutPath $INSTDIR + + !insertmacro wails.files + + CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + + !insertmacro wails.associateFiles + !insertmacro wails.associateCustomProtocols + + !insertmacro wails.writeUninstaller +SectionEnd + +Section "uninstall" + !insertmacro wails.setShellContext + + RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath + + RMDir /r $INSTDIR + + Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" + Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk" + + !insertmacro wails.unassociateFiles + !insertmacro wails.unassociateCustomProtocols + + !insertmacro wails.deleteUninstaller +SectionEnd diff --git a/examples/wails3_init_updater/build/windows/nsis/wails_tools.nsh b/examples/wails3_init_updater/build/windows/nsis/wails_tools.nsh new file mode 100644 index 0000000..e92794d --- /dev/null +++ b/examples/wails3_init_updater/build/windows/nsis/wails_tools.nsh @@ -0,0 +1,236 @@ +# DO NOT EDIT - Generated automatically by `wails build` + +!include "x64.nsh" +!include "WinVer.nsh" +!include "FileFunc.nsh" + +!ifndef INFO_PROJECTNAME + !define INFO_PROJECTNAME "updater-example" +!endif +!ifndef INFO_COMPANYNAME + !define INFO_COMPANYNAME "My Company" +!endif +!ifndef INFO_PRODUCTNAME + !define INFO_PRODUCTNAME "My Product" +!endif +!ifndef INFO_PRODUCTVERSION + !define INFO_PRODUCTVERSION "0.1.0" +!endif +!ifndef INFO_COPYRIGHT + !define INFO_COPYRIGHT "© 2026, My Company" +!endif +!ifndef PRODUCT_EXECUTABLE + !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe" +!endif +!ifndef UNINST_KEY_NAME + !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +!endif +!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}" + +!ifndef REQUEST_EXECUTION_LEVEL + !define REQUEST_EXECUTION_LEVEL "admin" +!endif + +RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" + +!ifdef ARG_WAILS_AMD64_BINARY + !define SUPPORTS_AMD64 +!endif + +!ifdef ARG_WAILS_ARM64_BINARY + !define SUPPORTS_ARM64 +!endif + +!ifdef SUPPORTS_AMD64 + !ifdef SUPPORTS_ARM64 + !define ARCH "amd64_arm64" + !else + !define ARCH "amd64" + !endif +!else + !ifdef SUPPORTS_ARM64 + !define ARCH "arm64" + !else + !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY" + !endif +!endif + +!macro wails.checkArchitecture + !ifndef WAILS_WIN10_REQUIRED + !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later." + !endif + + !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED + !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}" + !endif + + ${If} ${AtLeastWin10} + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + Goto ok + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + Goto ok + ${EndIf} + !endif + + IfSilent silentArch notSilentArch + silentArch: + SetErrorLevel 65 + Abort + notSilentArch: + MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}" + Quit + ${else} + IfSilent silentWin notSilentWin + silentWin: + SetErrorLevel 64 + Abort + notSilentWin: + MessageBox MB_OK "${WAILS_WIN10_REQUIRED}" + Quit + ${EndIf} + + ok: +!macroend + +!macro wails.files + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}" + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}" + ${EndIf} + !endif +!macroend + +!macro wails.writeUninstaller + WriteUninstaller "$INSTDIR\uninstall.exe" + + SetRegView 64 + WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}" + WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" + WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" + + ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 + IntFmt $0 "0x%08X" $0 + WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0" +!macroend + +!macro wails.deleteUninstaller + Delete "$INSTDIR\uninstall.exe" + + SetRegView 64 + DeleteRegKey HKLM "${UNINST_KEY}" +!macroend + +!macro wails.setShellContext + ${If} ${REQUEST_EXECUTION_LEVEL} == "admin" + SetShellVarContext all + ${else} + SetShellVarContext current + ${EndIf} +!macroend + +# Install webview2 by launching the bootstrapper +# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment +!macro wails.webview2runtime + !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT + !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime" + !endif + + SetRegView 64 + # If the admin key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + + ${If} ${REQUEST_EXECUTION_LEVEL} == "user" + # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + ${EndIf} + + SetDetailsPrint both + DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}" + SetDetailsPrint listonly + + InitPluginsDir + CreateDirectory "$pluginsdir\webview2bootstrapper" + SetOutPath "$pluginsdir\webview2bootstrapper" + File "MicrosoftEdgeWebview2Setup.exe" + ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install' + + SetDetailsPrint both + ok: +!macroend + +# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b +!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" + + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}" + + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open" + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}` +!macroend + +!macro APP_UNASSOCIATE EXT FILECLASS + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup` + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0" + + DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}` +!macroend + +!macro wails.associateFiles + ; Create file associations + +!macroend + +!macro wails.unassociateFiles + ; Delete app associations + +!macroend + +!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}" +!macroend + +!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" +!macroend + +!macro wails.associateCustomProtocols + ; Create custom protocols associations + +!macroend + +!macro wails.unassociateCustomProtocols + ; Delete app custom protocol associations + +!macroend \ No newline at end of file diff --git a/examples/wails3_init_updater/build/windows/wails.exe.manifest b/examples/wails3_init_updater/build/windows/wails.exe.manifest new file mode 100644 index 0000000..2704beb --- /dev/null +++ b/examples/wails3_init_updater/build/windows/wails.exe.manifest @@ -0,0 +1,22 @@ + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + + + + + + + + \ No newline at end of file diff --git a/examples/wails3_init_updater/cmd/mockupdateserver/main.go b/examples/wails3_init_updater/cmd/mockupdateserver/main.go new file mode 100644 index 0000000..3d79c15 --- /dev/null +++ b/examples/wails3_init_updater/cmd/mockupdateserver/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "log" + "net/http" + + "github.com/Eriyc/rules_wails/examples/wails3_init_updater/mockupdateserver" +) + +func main() { + server := &http.Server{ + Addr: "127.0.0.1:18765", + Handler: mockupdateserver.NewHandler(), + } + log.Println("mock updater server listening on http://127.0.0.1:18765") + log.Fatal(server.ListenAndServe()) +} diff --git a/examples/wails3_init_updater/frontend/Inter Font License.txt b/examples/wails3_init_updater/frontend/Inter Font License.txt new file mode 100644 index 0000000..b525cbf --- /dev/null +++ b/examples/wails3_init_updater/frontend/Inter Font License.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/examples/wails3_init_updater/frontend/bindings/github.com/Eriyc/rules_wails/examples/wails3_init_updater/index.ts b/examples/wails3_init_updater/frontend/bindings/github.com/Eriyc/rules_wails/examples/wails3_init_updater/index.ts new file mode 100644 index 0000000..1aeb987 --- /dev/null +++ b/examples/wails3_init_updater/frontend/bindings/github.com/Eriyc/rules_wails/examples/wails3_init_updater/index.ts @@ -0,0 +1,5 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +import * as UpdateService from "./updateservice.js"; +export { UpdateService }; diff --git a/examples/wails3_init_updater/frontend/bindings/github.com/Eriyc/rules_wails/examples/wails3_init_updater/updateservice.ts b/examples/wails3_init_updater/frontend/bindings/github.com/Eriyc/rules_wails/examples/wails3_init_updater/updateservice.ts new file mode 100644 index 0000000..9120946 --- /dev/null +++ b/examples/wails3_init_updater/frontend/bindings/github.com/Eriyc/rules_wails/examples/wails3_init_updater/updateservice.ts @@ -0,0 +1,39 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { + Call as $Call, + CancellablePromise as $CancellablePromise, + Create as $Create, +} from "@wailsio/runtime"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as updates$0 from "../../pkg/wails3kit/updates/models.js"; + +export function ApplyAndRestart(): $CancellablePromise { + return $Call.ByID(2468725628); +} + +export function Check(): $CancellablePromise { + return $Call.ByID(688929688).then(($result: any) => { + return $$createType0($result); + }); +} + +export function Download(): $CancellablePromise { + return $Call.ByID(137313328).then(($result: any) => { + return $$createType0($result); + }); +} + +export function Snapshot(): $CancellablePromise { + return $Call.ByID(270782772).then(($result: any) => { + return $$createType0($result); + }); +} + +// Private type creation functions +const $$createType0 = updates$0.Snapshot.createFrom; diff --git a/examples/wails3_init_updater/frontend/bindings/github.com/Eriyc/rules_wails/pkg/wails3kit/updates/index.ts b/examples/wails3_init_updater/frontend/bindings/github.com/Eriyc/rules_wails/pkg/wails3kit/updates/index.ts new file mode 100644 index 0000000..979513c --- /dev/null +++ b/examples/wails3_init_updater/frontend/bindings/github.com/Eriyc/rules_wails/pkg/wails3kit/updates/index.ts @@ -0,0 +1,17 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export { + Artifact, + ArtifactFormat, + ArtifactKind, + BundleFile, + BundleManifest, + Channel, + ErrorInfo, + InstallRoot, + Release, + Snapshot, + StagedArtifact, + State, +} from "./models.js"; diff --git a/examples/wails3_init_updater/frontend/bindings/github.com/Eriyc/rules_wails/pkg/wails3kit/updates/models.ts b/examples/wails3_init_updater/frontend/bindings/github.com/Eriyc/rules_wails/pkg/wails3kit/updates/models.ts new file mode 100644 index 0000000..c3c1f49 --- /dev/null +++ b/examples/wails3_init_updater/frontend/bindings/github.com/Eriyc/rules_wails/pkg/wails3kit/updates/models.ts @@ -0,0 +1,342 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Create as $Create } from "@wailsio/runtime"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as time$0 from "../../../../../../time/models.js"; + +export class Artifact { + kind: ArtifactKind; + format: ArtifactFormat; + url: string; + sha256: string; + size?: number; + + /** Creates a new Artifact instance. */ + constructor($$source: Partial = {}) { + if (!("kind" in $$source)) { + this["kind"] = ArtifactKind.$zero; + } + if (!("format" in $$source)) { + this["format"] = ArtifactFormat.$zero; + } + if (!("url" in $$source)) { + this["url"] = ""; + } + if (!("sha256" in $$source)) { + this["sha256"] = ""; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new Artifact instance from a string or object. + */ + static createFrom($$source: any = {}): Artifact { + let $$parsedSource = typeof $$source === "string" ? JSON.parse($$source) : $$source; + return new Artifact($$parsedSource as Partial); + } +} + +export enum ArtifactFormat { + /** + * The Go zero value for the underlying type of the enum. + */ + $zero = "", + + ArtifactFormatZip = "zip", + ArtifactFormatTarGz = "tar.gz", +} + +export enum ArtifactKind { + /** + * The Go zero value for the underlying type of the enum. + */ + $zero = "", + + ArtifactKindBundleArchive = "bundle-archive", +} + +export class BundleFile { + path: string; + mode: string; + + /** Creates a new BundleFile instance. */ + constructor($$source: Partial = {}) { + if (!("path" in $$source)) { + this["path"] = ""; + } + if (!("mode" in $$source)) { + this["mode"] = ""; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new BundleFile instance from a string or object. + */ + static createFrom($$source: any = {}): BundleFile { + let $$parsedSource = typeof $$source === "string" ? JSON.parse($$source) : $$source; + return new BundleFile($$parsedSource as Partial); + } +} + +export class BundleManifest { + schemaVersion: number; + entrypoint: string; + files: BundleFile[]; + + /** Creates a new BundleManifest instance. */ + constructor($$source: Partial = {}) { + if (!("schemaVersion" in $$source)) { + this["schemaVersion"] = 0; + } + if (!("entrypoint" in $$source)) { + this["entrypoint"] = ""; + } + if (!("files" in $$source)) { + this["files"] = []; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new BundleManifest instance from a string or object. + */ + static createFrom($$source: any = {}): BundleManifest { + const $$createField2_0 = $$createType1; + let $$parsedSource = typeof $$source === "string" ? JSON.parse($$source) : $$source; + if ("files" in $$parsedSource) { + $$parsedSource["files"] = $$createField2_0($$parsedSource["files"]); + } + return new BundleManifest($$parsedSource as Partial); + } +} + +export enum Channel { + /** + * The Go zero value for the underlying type of the enum. + */ + $zero = "", + + ChannelStable = "stable", + ChannelBeta = "beta", + ChannelAlpha = "alpha", +} + +export class ErrorInfo { + code: string; + message: string; + + /** Creates a new ErrorInfo instance. */ + constructor($$source: Partial = {}) { + if (!("code" in $$source)) { + this["code"] = ""; + } + if (!("message" in $$source)) { + this["message"] = ""; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new ErrorInfo instance from a string or object. + */ + static createFrom($$source: any = {}): ErrorInfo { + let $$parsedSource = typeof $$source === "string" ? JSON.parse($$source) : $$source; + return new ErrorInfo($$parsedSource as Partial); + } +} + +export class InstallRoot { + path: string; + + /** Creates a new InstallRoot instance. */ + constructor($$source: Partial = {}) { + if (!("path" in $$source)) { + this["path"] = ""; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new InstallRoot instance from a string or object. + */ + static createFrom($$source: any = {}): InstallRoot { + let $$parsedSource = typeof $$source === "string" ? JSON.parse($$source) : $$source; + return new InstallRoot($$parsedSource as Partial); + } +} + +export class Release { + id: string; + version: string; + channel: Channel; + notesMarkdown?: string; + publishedAt: time$0.Time; + artifact: Artifact; + + /** Creates a new Release instance. */ + constructor($$source: Partial = {}) { + if (!("id" in $$source)) { + this["id"] = ""; + } + if (!("version" in $$source)) { + this["version"] = ""; + } + if (!("channel" in $$source)) { + this["channel"] = Channel.$zero; + } + if (!("publishedAt" in $$source)) { + this["publishedAt"] = null; + } + if (!("artifact" in $$source)) { + this["artifact"] = new Artifact(); + } + + Object.assign(this, $$source); + } + + /** + * Creates a new Release instance from a string or object. + */ + static createFrom($$source: any = {}): Release { + const $$createField5_0 = $$createType2; + let $$parsedSource = typeof $$source === "string" ? JSON.parse($$source) : $$source; + if ("artifact" in $$parsedSource) { + $$parsedSource["artifact"] = $$createField5_0($$parsedSource["artifact"]); + } + return new Release($$parsedSource as Partial); + } +} + +export class Snapshot { + state: State; + currentVersion: string; + channel: Channel; + lastCheckedAt?: time$0.Time | null; + candidate?: Release | null; + staged?: StagedArtifact | null; + lastError?: ErrorInfo | null; + + /** Creates a new Snapshot instance. */ + constructor($$source: Partial = {}) { + if (!("state" in $$source)) { + this["state"] = State.$zero; + } + if (!("currentVersion" in $$source)) { + this["currentVersion"] = ""; + } + if (!("channel" in $$source)) { + this["channel"] = Channel.$zero; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new Snapshot instance from a string or object. + */ + static createFrom($$source: any = {}): Snapshot { + const $$createField4_0 = $$createType4; + const $$createField5_0 = $$createType6; + const $$createField6_0 = $$createType8; + let $$parsedSource = typeof $$source === "string" ? JSON.parse($$source) : $$source; + if ("candidate" in $$parsedSource) { + $$parsedSource["candidate"] = $$createField4_0($$parsedSource["candidate"]); + } + if ("staged" in $$parsedSource) { + $$parsedSource["staged"] = $$createField5_0($$parsedSource["staged"]); + } + if ("lastError" in $$parsedSource) { + $$parsedSource["lastError"] = $$createField6_0($$parsedSource["lastError"]); + } + return new Snapshot($$parsedSource as Partial); + } +} + +export class StagedArtifact { + path: string; + root: InstallRoot; + release: Release; + bundle: BundleManifest; + + /** Creates a new StagedArtifact instance. */ + constructor($$source: Partial = {}) { + if (!("path" in $$source)) { + this["path"] = ""; + } + if (!("root" in $$source)) { + this["root"] = new InstallRoot(); + } + if (!("release" in $$source)) { + this["release"] = new Release(); + } + if (!("bundle" in $$source)) { + this["bundle"] = new BundleManifest(); + } + + Object.assign(this, $$source); + } + + /** + * Creates a new StagedArtifact instance from a string or object. + */ + static createFrom($$source: any = {}): StagedArtifact { + const $$createField1_0 = $$createType9; + const $$createField2_0 = $$createType3; + const $$createField3_0 = $$createType10; + let $$parsedSource = typeof $$source === "string" ? JSON.parse($$source) : $$source; + if ("root" in $$parsedSource) { + $$parsedSource["root"] = $$createField1_0($$parsedSource["root"]); + } + if ("release" in $$parsedSource) { + $$parsedSource["release"] = $$createField2_0($$parsedSource["release"]); + } + if ("bundle" in $$parsedSource) { + $$parsedSource["bundle"] = $$createField3_0($$parsedSource["bundle"]); + } + return new StagedArtifact($$parsedSource as Partial); + } +} + +export enum State { + /** + * The Go zero value for the underlying type of the enum. + */ + $zero = "", + + StateIdle = "idle", + StateChecking = "checking", + StateUpToDate = "up_to_date", + StateUpdateAvailable = "update_available", + StateDownloading = "downloading", + StateDownloaded = "downloaded", + StateVerifying = "verifying", + StateReadyToApply = "ready_to_apply", + StateApplying = "applying", + StateRestarting = "restarting", + StateFailed = "failed", +} + +// Private type creation functions +const $$createType0 = BundleFile.createFrom; +const $$createType1 = $Create.Array($$createType0); +const $$createType2 = Artifact.createFrom; +const $$createType3 = Release.createFrom; +const $$createType4 = $Create.Nullable($$createType3); +const $$createType5 = StagedArtifact.createFrom; +const $$createType6 = $Create.Nullable($$createType5); +const $$createType7 = ErrorInfo.createFrom; +const $$createType8 = $Create.Nullable($$createType7); +const $$createType9 = InstallRoot.createFrom; +const $$createType10 = BundleManifest.createFrom; diff --git a/examples/wails3_init_updater/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.ts b/examples/wails3_init_updater/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.ts new file mode 100644 index 0000000..1ea1058 --- /dev/null +++ b/examples/wails3_init_updater/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.ts @@ -0,0 +1,9 @@ +//@ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Create as $Create } from "@wailsio/runtime"; + +Object.freeze($Create.Events); diff --git a/examples/wails3_init_updater/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts b/examples/wails3_init_updater/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts new file mode 100644 index 0000000..3dd1807 --- /dev/null +++ b/examples/wails3_init_updater/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts @@ -0,0 +1,2 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT diff --git a/examples/wails3_init_updater/frontend/index.html b/examples/wails3_init_updater/frontend/index.html new file mode 100644 index 0000000..0140369 --- /dev/null +++ b/examples/wails3_init_updater/frontend/index.html @@ -0,0 +1,61 @@ + + + + + + + + Updater Example + + +
+
+

Wails 3 In-App Updates

+

Updater Example

+

+ This app starts from wails3 init -t vanilla-ts and binds the updater library + behind a single Wails service. +

+
+ +
+
+
+ Current Version + 0.1.0 +
+
+ Channel + stable +
+
+ State + idle +
+
+ +
+ + + +
+ +
+
+ Candidate Version +

none

+
+
+ Last Error +

none

+
+
+ Release Notes +

No release selected.

+
+
+
+
+ + + diff --git a/examples/wails3_init_updater/frontend/package.json b/examples/wails3_init_updater/frontend/package.json new file mode 100644 index 0000000..4037642 --- /dev/null +++ b/examples/wails3_init_updater/frontend/package.json @@ -0,0 +1,19 @@ +{ + "name": "frontend", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build:dev": "vite build --minify false --mode development", + "build": "vite build --mode production", + "preview": "vite preview" + }, + "dependencies": { + "@wailsio/runtime": "latest" + }, + "devDependencies": { + "typescript": "^4.9.3", + "vite": "^5.0.0" + } +} diff --git a/examples/wails3_init_updater/frontend/public/Inter-Medium.ttf b/examples/wails3_init_updater/frontend/public/Inter-Medium.ttf new file mode 100644 index 0000000..a01f377 Binary files /dev/null and b/examples/wails3_init_updater/frontend/public/Inter-Medium.ttf differ diff --git a/examples/wails3_init_updater/frontend/public/style.css b/examples/wails3_init_updater/frontend/public/style.css new file mode 100644 index 0000000..6167d59 --- /dev/null +++ b/examples/wails3_init_updater/frontend/public/style.css @@ -0,0 +1,122 @@ +:root { + font-family: "Inter", sans-serif; + font-size: 16px; + line-height: 1.5; + color: #1d1e1a; + background: + radial-gradient(circle at top left, rgba(208, 109, 75, 0.24), transparent 35%), + radial-gradient(circle at top right, rgba(70, 110, 98, 0.18), transparent 30%), + linear-gradient(180deg, #f4efe6 0%, #efe3d1 100%); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 400; + src: + local(""), + url("./Inter-Medium.ttf") format("truetype"); +} + +body { + margin: 0; + min-height: 100vh; +} + +button { + border: 0; + border-radius: 999px; + padding: 0.85rem 1.35rem; + cursor: pointer; + font: inherit; +} + +.layout { + max-width: 860px; + margin: 0 auto; + padding: 4rem 1.5rem; +} + +.hero h1 { + margin: 0; + font-size: clamp(2.8rem, 7vw, 5rem); + line-height: 0.96; +} + +.eyebrow, +.label { + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 0.8rem; + color: #6b5d4e; +} + +.lede { + max-width: 52ch; + color: #433a30; +} + +.panel { + margin-top: 2rem; + padding: 1.5rem; + border: 1px solid rgba(77, 61, 47, 0.16); + border-radius: 28px; + background: rgba(255, 250, 244, 0.78); + backdrop-filter: blur(12px); + box-shadow: 0 32px 80px rgba(90, 69, 53, 0.15); +} + +.stat-grid { + display: grid; + gap: 1rem; + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.stat { + padding: 1rem; + border-radius: 20px; + background: rgba(255, 255, 255, 0.62); +} + +.stat strong { + display: block; + margin-top: 0.45rem; + font-size: 1.4rem; +} + +.actions { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; + margin-top: 1.5rem; +} + +.btn { + color: #f8f3ec; + background: #294b47; +} + +.btn.danger { + background: #9e5237; +} + +.details { + display: grid; + gap: 1rem; + margin-top: 1.5rem; +} + +.details p { + margin: 0.35rem 0 0; + color: #362d24; +} + +@media (max-width: 720px) { + .stat-grid { + grid-template-columns: 1fr; + } +} diff --git a/examples/wails3_init_updater/frontend/public/typescript.svg b/examples/wails3_init_updater/frontend/public/typescript.svg new file mode 100644 index 0000000..d91c910 --- /dev/null +++ b/examples/wails3_init_updater/frontend/public/typescript.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/wails3_init_updater/frontend/public/wails.png b/examples/wails3_init_updater/frontend/public/wails.png new file mode 100644 index 0000000..8bdf424 Binary files /dev/null and b/examples/wails3_init_updater/frontend/public/wails.png differ diff --git a/examples/wails3_init_updater/frontend/src/main.ts b/examples/wails3_init_updater/frontend/src/main.ts new file mode 100644 index 0000000..f8fc8a6 --- /dev/null +++ b/examples/wails3_init_updater/frontend/src/main.ts @@ -0,0 +1,62 @@ +import { Events } from "@wailsio/runtime"; +import { UpdateService } from "../bindings/github.com/Eriyc/rules_wails/examples/wails3_init_updater"; + +type Snapshot = { + state: string; + currentVersion: string; + channel: string; + candidate?: { + version: string; + notesMarkdown?: string; + }; + lastError?: { + message: string; + }; +}; + +const currentVersionElement = document.getElementById("current-version")! as HTMLDivElement; +const channelElement = document.getElementById("channel")! as HTMLDivElement; +const stateElement = document.getElementById("state")! as HTMLDivElement; +const candidateVersionElement = document.getElementById("candidate-version")! as HTMLDivElement; +const lastErrorElement = document.getElementById("last-error")! as HTMLDivElement; +const releaseNotesElement = document.getElementById("release-notes")! as HTMLDivElement; +const checkButton = document.getElementById("check")! as HTMLButtonElement; +const downloadButton = document.getElementById("download")! as HTMLButtonElement; +const applyButton = document.getElementById("apply")! as HTMLButtonElement; + +function render(snapshot: Snapshot) { + currentVersionElement.innerText = snapshot.currentVersion ?? "unknown"; + channelElement.innerText = snapshot.channel ?? "unknown"; + stateElement.innerText = snapshot.state ?? "idle"; + candidateVersionElement.innerText = snapshot.candidate?.version ?? "none"; + lastErrorElement.innerText = snapshot.lastError?.message ?? "none"; + releaseNotesElement.innerText = snapshot.candidate?.notesMarkdown ?? "No release selected."; +} + +async function refreshSnapshot() { + render((await UpdateService.Snapshot()) as Snapshot); +} + +checkButton.addEventListener("click", async () => { + render((await UpdateService.Check()) as Snapshot); +}); + +downloadButton.addEventListener("click", async () => { + render((await UpdateService.Download()) as Snapshot); +}); + +applyButton.addEventListener("click", async () => { + try { + await UpdateService.ApplyAndRestart(); + } catch (error) { + lastErrorElement.innerText = String(error); + } +}); + +Events.On("updates:state", (event) => { + render(event.data as Snapshot); +}); + +refreshSnapshot().catch((error) => { + lastErrorElement.innerText = String(error); +}); diff --git a/examples/wails3_init_updater/frontend/src/vite-env.d.ts b/examples/wails3_init_updater/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/examples/wails3_init_updater/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/wails3_init_updater/frontend/tsconfig.json b/examples/wails3_init_updater/frontend/tsconfig.json new file mode 100644 index 0000000..29f4fba --- /dev/null +++ b/examples/wails3_init_updater/frontend/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "allowJs": true, + + "noEmit": true, + "skipLibCheck": true, + + "module": "ESNext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + + "lib": ["DOM", "DOM.Iterable", "ESNext"], + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": false, + "noImplicitAny": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "bindings"] +} diff --git a/examples/wails3_init_updater/frontend/vite.config.ts b/examples/wails3_init_updater/frontend/vite.config.ts new file mode 100644 index 0000000..56f2d6a --- /dev/null +++ b/examples/wails3_init_updater/frontend/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vite"; +import wails from "@wailsio/runtime/plugins/vite"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [wails("./bindings")], +}); diff --git a/examples/wails3_init_updater/go.mod b/examples/wails3_init_updater/go.mod new file mode 100644 index 0000000..709cd7e --- /dev/null +++ b/examples/wails3_init_updater/go.mod @@ -0,0 +1,53 @@ +module github.com/Eriyc/rules_wails/examples/wails3_init_updater + +go 1.26 + +require ( + github.com/Eriyc/rules_wails v0.0.0 + github.com/wailsapp/wails/v3 v3.0.0-alpha.74 +) + +replace github.com/Eriyc/rules_wails => ../.. + +require ( + dario.cat/mergo v1.0.2 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.3.0 // indirect + github.com/adrg/xdg v0.5.3 // indirect + github.com/bep/debounce v1.2.1 // indirect + github.com/cloudflare/circl v1.6.3 // indirect + github.com/coder/websocket v1.8.14 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect + github.com/ebitengine/purego v0.9.1 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.7.0 // indirect + github.com/go-git/go-git/v5 v5.16.4 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/godbus/dbus/v5 v5.2.2 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect + github.com/kevinburke/ssh_config v1.4.0 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leaanthony/go-ansi-parser v1.6.1 // indirect + github.com/leaanthony/u v1.1.1 // indirect + github.com/lmittmann/tint v1.1.2 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pjbgf/sha1cd v0.5.0 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/samber/lo v1.52.0 // indirect + github.com/sergi/go-diff v1.4.0 // indirect + github.com/skeema/knownhosts v1.3.2 // indirect + github.com/wailsapp/go-webview2 v1.0.23 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + golang.org/x/crypto v0.47.0 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.33.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect +) diff --git a/examples/wails3_init_updater/go.sum b/examples/wails3_init_updater/go.sum new file mode 100644 index 0000000..8ed09d8 --- /dev/null +++ b/examples/wails3_init_updater/go.sum @@ -0,0 +1,151 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= +github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= +github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= +github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= +github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= +github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM= +github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y= +github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= +github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU= +github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= +github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ= +github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= +github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= +github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= +github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= +github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= +github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w= +github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= +github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= +github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= +github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg= +github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/wailsapp/go-webview2 v1.0.23 h1:jmv8qhz1lHibCc79bMM/a/FqOnnzOGEisLav+a0b9P0= +github.com/wailsapp/go-webview2 v1.0.23/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= +github.com/wailsapp/wails/v3 v3.0.0-alpha.74 h1:wRm1EiDQtxDisXk46NtpiBH90STwfKp36NrTDwOEdxw= +github.com/wailsapp/wails/v3 v3.0.0-alpha.74/go.mod h1:4saK4A4K9970X+X7RkMwP2lyGbLogcUz54wVeq4C/V8= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= +golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/wails3_init_updater/main.go b/examples/wails3_init_updater/main.go new file mode 100644 index 0000000..290ea47 --- /dev/null +++ b/examples/wails3_init_updater/main.go @@ -0,0 +1,78 @@ +package main + +import ( + "embed" + _ "embed" + "log" + + updatesbootstrap "github.com/Eriyc/rules_wails/pkg/wails3kit/updates/bootstrap" + updateswails "github.com/Eriyc/rules_wails/pkg/wails3kit/updates/wails" + "github.com/wailsapp/wails/v3/pkg/application" +) + +//go:embed all:frontend/dist +var assets embed.FS + +func init() { + updateswails.RegisterEvents("updates:state") +} + +func main() { + handled, err := updatesbootstrap.MaybeRun() + if err != nil { + log.Fatal(err) + } + if handled { + return + } + + appDescriptor, err := currentAppDescriptor() + if err != nil { + log.Fatal(err) + } + + controller, err := newController(appDescriptor) + if err != nil { + log.Fatal(err) + } + + var app *application.App + updateService := NewUpdateService(updateswails.NewService(updateswails.Options{ + Controller: controller, + EventName: "updates:state", + Emitter: func(name string, data any) { + if app != nil { + app.Event.Emit(name, data) + } + }, + })) + + app = application.New(application.Options{ + Name: "updater-example", + Description: "A Wails 3 updater example backed by the local update library", + Services: []application.Service{ + application.NewService(updateService), + }, + Assets: application.AssetOptions{ + Handler: application.AssetFileServerFS(assets), + }, + Mac: application.MacOptions{ + ApplicationShouldTerminateAfterLastWindowClosed: true, + }, + }) + + app.Window.NewWithOptions(application.WebviewWindowOptions{ + Title: "Updater Example", + Mac: application.MacWindow{ + InvisibleTitleBarHeight: 50, + Backdrop: application.MacBackdropTranslucent, + TitleBar: application.MacTitleBarHiddenInset, + }, + BackgroundColour: application.NewRGB(244, 239, 230), + URL: "/", + }) + + if err := app.Run(); err != nil { + log.Fatal(err) + } +} diff --git a/examples/wails3_init_updater/mockupdateserver/server.go b/examples/wails3_init_updater/mockupdateserver/server.go new file mode 100644 index 0000000..ba24683 --- /dev/null +++ b/examples/wails3_init_updater/mockupdateserver/server.go @@ -0,0 +1,150 @@ +package mockupdateserver + +import ( + "archive/zip" + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "net/http" + "runtime" + + "github.com/Eriyc/rules_wails/pkg/wails3kit/updates" +) + +const BearerToken = "test-token" + +func NewHandler() http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("/manifest.json", manifest) + mux.HandleFunc("/artifacts/updater-example_0.2.0.zip", artifact) + mux.HandleFunc("/artifacts/updater-example_0.2.0.zip.sha256", checksum) + + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + if request.Header.Get("Authorization") != "Bearer "+BearerToken { + http.Error(writer, "unauthorized", http.StatusUnauthorized) + return + } + mux.ServeHTTP(writer, request) + }) +} + +func manifest(writer http.ResponseWriter, request *http.Request) { + _, checksumValue, err := archiveWithChecksum() + if err != nil { + http.Error(writer, err.Error(), http.StatusInternalServerError) + return + } + + document := map[string]any{ + "schemaVersion": 1, + "productID": "com.eriyc.updater-example", + "releases": []any{ + map[string]any{ + "id": "0.2.0", + "version": "0.2.0", + "channel": "stable", + "publishedAt": "2026-03-01T03:10:56Z", + "notesMarkdown": "Adds a staged resource update and relaunch flow.", + "artifacts": []any{ + map[string]any{ + "os": runtime.GOOS, + "arch": runtime.GOARCH, + "kind": updates.ArtifactKindBundleArchive, + "format": updates.ArtifactFormatZip, + "url": baseURL(request) + "/artifacts/updater-example_0.2.0.zip", + "sha256": checksumValue, + }, + }, + }, + }, + } + writer.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(writer).Encode(document) +} + +func artifact(writer http.ResponseWriter, _ *http.Request) { + archive, err := buildArchive() + if err != nil { + http.Error(writer, err.Error(), http.StatusInternalServerError) + return + } + writer.Header().Set("Content-Type", "application/zip") + _, _ = writer.Write(archive) +} + +func checksum(writer http.ResponseWriter, _ *http.Request) { + _, checksumValue, err := archiveWithChecksum() + if err != nil { + http.Error(writer, err.Error(), http.StatusInternalServerError) + return + } + _, _ = writer.Write([]byte(checksumValue + " updater-example_0.2.0.zip")) +} + +func buildArchive() ([]byte, error) { + buffer := bytes.NewBuffer(nil) + archive := zip.NewWriter(buffer) + + bundleFile, err := archive.Create("bundle.json") + if err != nil { + return nil, err + } + bundle := updates.BundleManifest{ + SchemaVersion: 1, + EntryPoint: entryPoint(), + Files: []updates.BundleFile{ + {Path: resourcePath(), Mode: "0644"}, + }, + } + if err := json.NewEncoder(bundleFile).Encode(bundle); err != nil { + return nil, err + } + + resourceFile, err := archive.Create(resourcePath()) + if err != nil { + return nil, err + } + if _, err := resourceFile.Write([]byte(`{"version":"0.2.0","status":"downloaded"}`)); err != nil { + return nil, err + } + + if err := archive.Close(); err != nil { + return nil, err + } + return buffer.Bytes(), nil +} + +func archiveWithChecksum() ([]byte, string, error) { + archive, err := buildArchive() + if err != nil { + return nil, "", err + } + sum := sha256.Sum256(archive) + return archive, hex.EncodeToString(sum[:]), nil +} + +func entryPoint() string { + if runtime.GOOS == "darwin" { + return "Contents/MacOS/updater-example" + } + if runtime.GOOS == "windows" { + return "updater-example.exe" + } + return "updater-example" +} + +func resourcePath() string { + if runtime.GOOS == "darwin" { + return "Contents/Resources/update.json" + } + return "resources/update.json" +} + +func baseURL(request *http.Request) string { + scheme := "http" + if request.TLS != nil { + scheme = "https" + } + return scheme + "://" + request.Host +} diff --git a/examples/wails3_init_updater/update_service.go b/examples/wails3_init_updater/update_service.go new file mode 100644 index 0000000..08bdb41 --- /dev/null +++ b/examples/wails3_init_updater/update_service.go @@ -0,0 +1,41 @@ +package main + +import ( + "context" + + "github.com/Eriyc/rules_wails/pkg/wails3kit/updates" + updateswails "github.com/Eriyc/rules_wails/pkg/wails3kit/updates/wails" + "github.com/wailsapp/wails/v3/pkg/application" +) + +type UpdateService struct { + service *updateswails.Service +} + +func NewUpdateService(service *updateswails.Service) *UpdateService { + return &UpdateService{service: service} +} + +func (service *UpdateService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { + return service.service.ServiceStartup(ctx, options) +} + +func (service *UpdateService) ServiceShutdown() error { + return service.service.ServiceShutdown() +} + +func (service *UpdateService) Snapshot() updates.Snapshot { + return service.service.Snapshot() +} + +func (service *UpdateService) Check() (updates.Snapshot, error) { + return service.service.Check() +} + +func (service *UpdateService) Download() (updates.Snapshot, error) { + return service.service.Download() +} + +func (service *UpdateService) ApplyAndRestart() error { + return service.service.ApplyAndRestart() +} diff --git a/examples/wails3_init_updater/updater.go b/examples/wails3_init_updater/updater.go new file mode 100644 index 0000000..17d8721 --- /dev/null +++ b/examples/wails3_init_updater/updater.go @@ -0,0 +1,104 @@ +package main + +import ( + "fmt" + "net/http" + "os" + "path/filepath" + "runtime" + + "github.com/Eriyc/rules_wails/pkg/wails3kit/updates" + updatesdarwin "github.com/Eriyc/rules_wails/pkg/wails3kit/updates/platform/darwin" + updateslinux "github.com/Eriyc/rules_wails/pkg/wails3kit/updates/platform/linux" + updateswindows "github.com/Eriyc/rules_wails/pkg/wails3kit/updates/platform/windows" + httpmanifest "github.com/Eriyc/rules_wails/pkg/wails3kit/updates/providers/httpmanifest" + filestore "github.com/Eriyc/rules_wails/pkg/wails3kit/updates/storage/file" +) + +const ( + productID = "com.eriyc.updater-example" + currentVersion = "0.1.0" +) + +type updaterSettings struct { + ManifestURL string + Token string +} + +func currentAppDescriptor() (updates.AppDescriptor, error) { + executablePath, err := os.Executable() + if err != nil { + return updates.AppDescriptor{}, err + } + workingDirectory, err := os.Getwd() + if err != nil { + return updates.AppDescriptor{}, err + } + + return updates.AppDescriptor{ + ProductID: productID, + CurrentVersion: currentVersion, + Channel: updates.ChannelStable, + OS: runtime.GOOS, + Arch: runtime.GOARCH, + ExecutablePath: executablePath, + Args: os.Args[1:], + WorkingDirectory: workingDirectory, + }, nil +} + +func loadUpdaterSettings() updaterSettings { + settings := updaterSettings{ + ManifestURL: os.Getenv("UPDATER_MANIFEST_URL"), + Token: os.Getenv("UPDATER_TOKEN"), + } + if settings.ManifestURL == "" { + settings.ManifestURL = "http://127.0.0.1:18765/manifest.json" + } + if settings.Token == "" { + settings.Token = "test-token" + } + return settings +} + +func newController(app updates.AppDescriptor) (*updates.Controller, error) { + settings := loadUpdaterSettings() + + provider, err := httpmanifest.New(httpmanifest.Config{ + ManifestURL: settings.ManifestURL, + HTTPClient: http.DefaultClient, + PrepareRequest: func(request *http.Request) error { + request.Header.Set("Authorization", "Bearer "+settings.Token) + return nil + }, + }) + if err != nil { + return nil, err + } + + platformInstaller, err := currentPlatformInstaller() + if err != nil { + return nil, err + } + + return updates.NewController(updates.Config{ + App: app, + Provider: provider, + Downloader: updates.NewHTTPDownloader(http.DefaultClient), + Store: filestore.New(filepath.Join(os.TempDir(), "wails3kit-example", "snapshot.json")), + Platform: platformInstaller, + }) +} + +func currentPlatformInstaller() (updates.PlatformInstaller, error) { + switch runtime.GOOS { + case "darwin": + return updatesdarwin.New(), nil + case "linux": + return updateslinux.New(), nil + case "windows": + return updateswindows.New(), nil + default: + return nil, fmt.Errorf("unsupported platform: %s", runtime.GOOS) + } +} diff --git a/examples/wails3_init_updater/updater_test.go b/examples/wails3_init_updater/updater_test.go new file mode 100644 index 0000000..a9719c0 --- /dev/null +++ b/examples/wails3_init_updater/updater_test.go @@ -0,0 +1,58 @@ +package main + +import ( + "net/http/httptest" + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/Eriyc/rules_wails/examples/wails3_init_updater/mockupdateserver" + "github.com/Eriyc/rules_wails/pkg/wails3kit/updates" + updateswails "github.com/Eriyc/rules_wails/pkg/wails3kit/updates/wails" +) + +func TestNewControllerAndService(t *testing.T) { + server := httptest.NewServer(mockupdateserver.NewHandler()) + defer server.Close() + + t.Setenv("UPDATER_MANIFEST_URL", server.URL+"/manifest.json") + t.Setenv("UPDATER_TOKEN", mockupdateserver.BearerToken) + + executablePath := filepath.Join(t.TempDir(), entryPointName()) + if err := os.WriteFile(executablePath, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil { + t.Fatalf("WriteFile returned error: %v", err) + } + + controller, err := newController(testDescriptor(t, executablePath)) + if err != nil { + t.Fatalf("newController returned error: %v", err) + } + + service := NewUpdateService(updateswails.NewService(updateswails.Options{ + Controller: controller, + })) + if service.Snapshot().CurrentVersion != currentVersion { + t.Fatalf("unexpected current version: %s", service.Snapshot().CurrentVersion) + } +} + +func testDescriptor(t *testing.T, executablePath string) updates.AppDescriptor { + t.Helper() + return updates.AppDescriptor{ + ProductID: productID, + CurrentVersion: currentVersion, + Channel: updates.ChannelStable, + OS: runtime.GOOS, + Arch: runtime.GOARCH, + ExecutablePath: executablePath, + WorkingDirectory: t.TempDir(), + } +} + +func entryPointName() string { + if runtime.GOOS == "windows" { + return "updater-example.exe" + } + return "updater-example" +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..0b2ca21 --- /dev/null +++ b/flake.lock @@ -0,0 +1,159 @@ +{ + "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", + "owner": "NixOS", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "flake-compat", + "type": "github" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1772024342, + "narHash": "sha256-+eXlIc4/7dE6EcPs9a2DaSY3fTA9AE526hGqkNID3Wg=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "6e34e97ed9788b17796ee43ccdbaf871a5c2b476", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "repo-lib", + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1772542754, + "narHash": "sha256-WGV2hy+VIeQsYXpsLjdr4GvHv5eECMISX1zKLTedhdg=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "8c809a146a140c5c8806f13399592dbcb1bb5dc4", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1770073757, + "narHash": "sha256-Vy+G+F+3E/Tl+GMNgiHl9Pah2DgShmIUBJXmbiQPHbI=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "47472570b1e607482890801aeaf29bfb749884f6", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { + "locked": { + "lastModified": 1770107345, + "narHash": "sha256-tbS0Ebx2PiA1FRW8mt8oejR0qMXmziJmPaU1d4kYY9g=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "4533d9293756b63904b7238acb84ac8fe4c8c2c4", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "repo-lib": { + "inputs": { + "git-hooks": "git-hooks", + "nixpkgs": [ + "nixpkgs" + ], + "treefmt-nix": "treefmt-nix" + }, + "locked": { + "lastModified": 1772866275, + "narHash": "sha256-lsJrFIbq6OO5wUC648VnvOmJm3qgJrlEugbdjeZsP34=", + "ref": "v3.0.0", + "rev": "96d2d190466dddcb9e652c38b70152f09b9fcb05", + "revCount": 50, + "type": "git", + "url": "https://git.dgren.dev/eric/nix-flake-lib" + }, + "original": { + "ref": "v3.0.0", + "type": "git", + "url": "https://git.dgren.dev/eric/nix-flake-lib" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "repo-lib": "repo-lib" + } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": "nixpkgs_3" + }, + "locked": { + "lastModified": 1770228511, + "narHash": "sha256-wQ6NJSuFqAEmIg2VMnLdCnUc0b7vslUohqqGGD+Fyxk=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "337a4fe074be1042a35086f15481d763b8ddc0e7", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..085d98f --- /dev/null +++ b/flake.nix @@ -0,0 +1,128 @@ +{ + description = "rules_wails development flake"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; + repo-lib.url = "git+https://git.dgren.dev/eric/nix-flake-lib?ref=v3.0.0"; + repo-lib.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = + { + self, + nixpkgs, + repo-lib, + ... + }: + repo-lib.lib.mkRepo { + inherit self nixpkgs; + src = ./.; + systems = repo-lib.lib.systems.default; + + config = { + shell.extraShellText = '' + export USE_BAZEL_VERSION="''${USE_BAZEL_VERSION:-9.0.0}" + export BUN_INSTALL="''${BUN_INSTALL:-$HOME/.bun}" + export PATH="$BUN_INSTALL/bin:$PATH" + ''; + + formatting = { + programs = { + oxfmt.enable = true; + shfmt.enable = true; + }; + + settings = { + shfmt.options = [ + "-i" + "2" + "-s" + "-w" + ]; + oxfmt.includes = [ + "*.md" + "*.yaml" + "*.yml" + "*.json" + ]; + }; + }; + }; + + perSystem = + { + pkgs, + ... + }: + let + bazel9 = pkgs.writeShellScriptBin "bazel" '' + export USE_BAZEL_VERSION="''${USE_BAZEL_VERSION:-9.0.0}" + exec ${pkgs.bazelisk}/bin/bazelisk "$@" + ''; + wails3 = pkgs.buildGoModule { + pname = "wails3"; + version = "3.0.0-alpha.74"; + src = pkgs.fetchFromGitHub { + owner = "wailsapp"; + repo = "wails"; + rev = "v3.0.0-alpha.74"; + hash = "sha256-7cRtJdv7UXi8JEJDC9f6WHrVhU7nDVk+dBUhRenZBc4="; + }; + modRoot = "v3"; + subPackages = [ "cmd/wails3" ]; + vendorHash = "sha256-WdragX08M/Fmw/IB6Atw27b1PPrQPNo2i19ykQLo8O0="; + go = pkgs.go_1_26; + meta.mainProgram = "wails3"; + }; + in + { + tools = [ + (repo-lib.lib.tools.fromPackage { + name = "Bun"; + package = pkgs.bun; + version.args = [ "--version" ]; + banner.color = "YELLOW"; + }) + (repo-lib.lib.tools.fromPackage { + name = "Go"; + package = pkgs.go_1_26; + version.args = [ "version" ]; + banner.color = "CYAN"; + }) + (repo-lib.lib.tools.fromPackage { + name = "Bazel"; + package = bazel9; + version.args = [ "--version" ]; + banner.color = "BLUE"; + }) + (repo-lib.lib.tools.fromPackage { + name = "Wails"; + package = wails3; + version.args = [ "version" ]; + banner.color = "MAGENTA"; + }) + ]; + + shell.packages = [ + pkgs.bun + pkgs.go_1_26 + pkgs.gopls + pkgs.gotools + pkgs.bazel-buildtools + pkgs.bazel-watcher + pkgs.oxfmt + bazel9 + wails3 + ]; + + checks.tests = { + command = "bazel build //wails/... //wails_bun/... //examples/..."; + stage = "pre-push"; + passFilenames = false; + runtimeInputs = [ bazel9 ]; + }; + + packages.wails3 = wails3; + }; + }; +} diff --git a/go.mod b/go.mod index b1b4a94..38d8737 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,50 @@ module github.com/Eriyc/rules_wails go 1.26 + +require ( + github.com/wailsapp/wails/v3 v3.0.0-alpha.74 + golang.org/x/mod v0.32.0 +) + +require ( + dario.cat/mergo v1.0.2 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.3.0 // indirect + github.com/adrg/xdg v0.5.3 // indirect + github.com/bep/debounce v1.2.1 // indirect + github.com/cloudflare/circl v1.6.3 // indirect + github.com/coder/websocket v1.8.14 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect + github.com/ebitengine/purego v0.9.1 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.7.0 // indirect + github.com/go-git/go-git/v5 v5.16.4 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/godbus/dbus/v5 v5.2.2 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect + github.com/kevinburke/ssh_config v1.4.0 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leaanthony/go-ansi-parser v1.6.1 // indirect + github.com/leaanthony/u v1.1.1 // indirect + github.com/lmittmann/tint v1.1.2 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pjbgf/sha1cd v0.5.0 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/samber/lo v1.52.0 // indirect + github.com/sergi/go-diff v1.4.0 // indirect + github.com/skeema/knownhosts v1.3.2 // indirect + github.com/wailsapp/go-webview2 v1.0.23 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + golang.org/x/crypto v0.47.0 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.33.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8ed09d8 --- /dev/null +++ b/go.sum @@ -0,0 +1,151 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= +github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= +github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= +github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= +github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= +github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM= +github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y= +github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= +github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU= +github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= +github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ= +github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= +github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= +github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= +github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= +github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= +github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w= +github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= +github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= +github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= +github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg= +github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/wailsapp/go-webview2 v1.0.23 h1:jmv8qhz1lHibCc79bMM/a/FqOnnzOGEisLav+a0b9P0= +github.com/wailsapp/go-webview2 v1.0.23/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= +github.com/wailsapp/wails/v3 v3.0.0-alpha.74 h1:wRm1EiDQtxDisXk46NtpiBH90STwfKp36NrTDwOEdxw= +github.com/wailsapp/wails/v3 v3.0.0-alpha.74/go.mod h1:4saK4A4K9970X+X7RkMwP2lyGbLogcUz54wVeq4C/V8= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= +golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/wails3kit/updates/BUILD.bazel b/pkg/wails3kit/updates/BUILD.bazel new file mode 100644 index 0000000..bc0a011 --- /dev/null +++ b/pkg/wails3kit/updates/BUILD.bazel @@ -0,0 +1,24 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "updates", + srcs = [ + "controller.go", + "downloader.go", + "errors.go", + "semver.go", + "types.go", + ], + importpath = "github.com/Eriyc/rules_wails/pkg/wails3kit/updates", + visibility = ["//visibility:public"], + deps = [ + "@org_golang_x_mod//semver", + ], +) + +go_test( + name = "updates_test", + srcs = ["controller_test.go"], + embed = [":updates"], + deps = [], +) diff --git a/pkg/wails3kit/updates/bootstrap/BUILD.bazel b/pkg/wails3kit/updates/bootstrap/BUILD.bazel new file mode 100644 index 0000000..528e27c --- /dev/null +++ b/pkg/wails3kit/updates/bootstrap/BUILD.bazel @@ -0,0 +1,9 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "bootstrap", + srcs = ["bootstrap.go"], + importpath = "github.com/Eriyc/rules_wails/pkg/wails3kit/updates/bootstrap", + visibility = ["//visibility:public"], + deps = ["//pkg/wails3kit/updates/platform"], +) diff --git a/pkg/wails3kit/updates/bootstrap/bootstrap.go b/pkg/wails3kit/updates/bootstrap/bootstrap.go new file mode 100644 index 0000000..d67baf1 --- /dev/null +++ b/pkg/wails3kit/updates/bootstrap/bootstrap.go @@ -0,0 +1,11 @@ +package bootstrap + +import ( + "os" + + "github.com/Eriyc/rules_wails/pkg/wails3kit/updates/platform" +) + +func MaybeRun() (bool, error) { + return platform.MaybeRun(os.Args) +} diff --git a/pkg/wails3kit/updates/controller.go b/pkg/wails3kit/updates/controller.go new file mode 100644 index 0000000..9abbac2 --- /dev/null +++ b/pkg/wails3kit/updates/controller.go @@ -0,0 +1,315 @@ +package updates + +import ( + "context" + "fmt" + "log/slog" + "os" + "sync" + "time" +) + +type Controller struct { + app AppDescriptor + provider ReleaseProvider + downloader Downloader + store SnapshotStore + platform PlatformInstaller + logger *slog.Logger + + mu sync.Mutex + snapshot Snapshot + busy bool + listeners map[int]chan Snapshot + nextListener int +} + +func NewController(cfg Config) (*Controller, error) { + if cfg.Provider == nil || cfg.Downloader == nil || cfg.Store == nil || cfg.Platform == nil { + return nil, ErrInvalidConfig + } + if _, err := NormalizeVersion(cfg.App.CurrentVersion); err != nil { + return nil, err + } + if cfg.App.WorkingDirectory == "" { + workingDirectory, err := os.Getwd() + if err == nil { + cfg.App.WorkingDirectory = workingDirectory + } + } + + logger := cfg.Logger + if logger == nil { + logger = slog.Default() + } + + snapshot, err := cfg.Store.Load(context.Background()) + if err != nil { + return nil, err + } + snapshot.CurrentVersion = cfg.App.CurrentVersion + snapshot.Channel = cfg.App.Channel + if snapshot.State == "" { + snapshot.State = StateIdle + } + + controller := &Controller{ + app: cfg.App, + provider: cfg.Provider, + downloader: cfg.Downloader, + store: cfg.Store, + platform: cfg.Platform, + logger: logger, + snapshot: snapshot, + listeners: make(map[int]chan Snapshot), + } + + if snapshot.Staged != nil { + comparison, err := CompareVersions(snapshot.Staged.Release.Version, cfg.App.CurrentVersion) + if err == nil && comparison == 0 { + snapshot.State = StateUpToDate + snapshot.Staged = nil + snapshot.Candidate = nil + snapshot.LastError = nil + controller.snapshot = snapshot + if err := controller.store.Save(context.Background(), snapshot); err != nil { + return nil, err + } + } + } + + return controller, nil +} + +func (controller *Controller) Snapshot() Snapshot { + controller.mu.Lock() + defer controller.mu.Unlock() + return controller.snapshot +} + +func (controller *Controller) Subscribe(buffer int) (<-chan Snapshot, func()) { + if buffer < 1 { + buffer = 1 + } + + controller.mu.Lock() + defer controller.mu.Unlock() + + controller.nextListener++ + id := controller.nextListener + channel := make(chan Snapshot, buffer) + channel <- controller.snapshot + controller.listeners[id] = channel + + return channel, func() { + controller.mu.Lock() + defer controller.mu.Unlock() + if _, ok := controller.listeners[id]; ok { + delete(controller.listeners, id) + } + } +} + +func (controller *Controller) Check(ctx context.Context, _ CheckRequest) (Snapshot, error) { + if !controller.beginOperation() { + return Snapshot{}, ErrBusy + } + defer controller.endOperation() + + now := time.Now().UTC() + controller.transition(StateChecking, nil, nil) + + release, err := controller.provider.Resolve(ctx, ResolveRequest{App: controller.app}) + if err != nil { + return controller.fail("resolve_failed", err) + } + + if release == nil { + snapshot := controller.currentSnapshot() + snapshot.State = StateUpToDate + snapshot.LastCheckedAt = &now + snapshot.Candidate = nil + snapshot.LastError = nil + if err := controller.save(snapshot); err != nil { + return Snapshot{}, err + } + return snapshot, nil + } + + comparison, err := CompareVersions(release.Version, controller.app.CurrentVersion) + if err != nil { + return controller.fail("invalid_release_version", err) + } + if comparison <= 0 { + snapshot := controller.currentSnapshot() + snapshot.State = StateUpToDate + snapshot.LastCheckedAt = &now + snapshot.Candidate = nil + snapshot.LastError = nil + if err := controller.save(snapshot); err != nil { + return Snapshot{}, err + } + return snapshot, nil + } + + snapshot := controller.currentSnapshot() + snapshot.State = StateUpdateAvailable + snapshot.LastCheckedAt = &now + snapshot.Candidate = release + snapshot.Staged = nil + snapshot.LastError = nil + if err := controller.save(snapshot); err != nil { + return Snapshot{}, err + } + + return snapshot, nil +} + +func (controller *Controller) Download(ctx context.Context) (Snapshot, error) { + if !controller.beginOperation() { + return Snapshot{}, ErrBusy + } + defer controller.endOperation() + + snapshot := controller.currentSnapshot() + if snapshot.Candidate == nil { + return Snapshot{}, ErrNoUpdate + } + + controller.transition(StateDownloading, snapshot.Candidate, nil) + + downloadedFile, err := controller.downloader.Download(ctx, snapshot.Candidate.Artifact) + if err != nil { + return controller.fail("download_failed", err) + } + if downloadedFile.SHA256 != "" && snapshot.Candidate.Artifact.SHA256 != "" && downloadedFile.SHA256 != snapshot.Candidate.Artifact.SHA256 { + return controller.fail("checksum_mismatch", ErrChecksumMismatch) + } + + controller.transition(StateVerifying, snapshot.Candidate, nil) + + root, err := controller.platform.DetectInstallRoot(controller.app) + if err != nil { + return controller.fail("detect_root_failed", err) + } + + stagedArtifact, err := controller.platform.Stage(ctx, root, downloadedFile, *snapshot.Candidate) + if err != nil { + return controller.fail("stage_failed", err) + } + + nextSnapshot := controller.currentSnapshot() + nextSnapshot.State = StateReadyToApply + nextSnapshot.Staged = &stagedArtifact + nextSnapshot.Candidate = &stagedArtifact.Release + nextSnapshot.LastError = nil + if err := controller.save(nextSnapshot); err != nil { + return Snapshot{}, err + } + + return nextSnapshot, nil +} + +func (controller *Controller) ApplyAndRestart(ctx context.Context) error { + if !controller.beginOperation() { + return ErrBusy + } + defer controller.endOperation() + + snapshot := controller.currentSnapshot() + if snapshot.Staged == nil { + return ErrNoStagedUpdate + } + + controller.transition(StateApplying, snapshot.Candidate, snapshot.Staged) + + if err := controller.platform.SpawnApplyAndRestart(ctx, ApplyRequest{ + App: controller.app, + Root: snapshot.Staged.Root, + Staged: *snapshot.Staged, + }); err != nil { + _, failErr := controller.fail("apply_failed", err) + return failErr + } + + nextSnapshot := controller.currentSnapshot() + nextSnapshot.State = StateRestarting + nextSnapshot.LastError = nil + return controller.save(nextSnapshot) +} + +func (controller *Controller) beginOperation() bool { + controller.mu.Lock() + defer controller.mu.Unlock() + if controller.busy { + return false + } + controller.busy = true + return true +} + +func (controller *Controller) endOperation() { + controller.mu.Lock() + defer controller.mu.Unlock() + controller.busy = false +} + +func (controller *Controller) currentSnapshot() Snapshot { + controller.mu.Lock() + defer controller.mu.Unlock() + return controller.snapshot +} + +func (controller *Controller) transition(state State, candidate *Release, staged *StagedArtifact) { + snapshot := controller.currentSnapshot() + snapshot.State = state + snapshot.Candidate = candidate + snapshot.Staged = staged + snapshot.LastError = nil + _ = controller.save(snapshot) +} + +func (controller *Controller) save(snapshot Snapshot) error { + snapshot.CurrentVersion = controller.app.CurrentVersion + snapshot.Channel = controller.app.Channel + if err := controller.store.Save(context.Background(), snapshot); err != nil { + return err + } + + controller.mu.Lock() + controller.snapshot = snapshot + listeners := make([]chan Snapshot, 0, len(controller.listeners)) + for _, listener := range controller.listeners { + listeners = append(listeners, listener) + } + controller.mu.Unlock() + + for _, listener := range listeners { + select { + case listener <- snapshot: + default: + select { + case <-listener: + default: + } + listener <- snapshot + } + } + + return nil +} + +func (controller *Controller) fail(code string, err error) (Snapshot, error) { + controller.logger.Error("update flow failed", "code", code, "error", err) + + snapshot := controller.currentSnapshot() + snapshot.State = StateFailed + snapshot.LastError = &ErrorInfo{ + Code: code, + Message: err.Error(), + } + if saveErr := controller.save(snapshot); saveErr != nil { + return Snapshot{}, fmt.Errorf("%w: %w", err, saveErr) + } + return snapshot, err +} diff --git a/pkg/wails3kit/updates/controller_test.go b/pkg/wails3kit/updates/controller_test.go new file mode 100644 index 0000000..bbe2dd3 --- /dev/null +++ b/pkg/wails3kit/updates/controller_test.go @@ -0,0 +1,242 @@ +package updates + +import ( + "context" + "errors" + "io" + "path/filepath" + "sync" + "testing" + "time" +) + +func TestNormalizeVersion(t *testing.T) { + t.Parallel() + + version, err := NormalizeVersion("1.2.3") + if err != nil { + t.Fatalf("NormalizeVersion returned error: %v", err) + } + if version != "v1.2.3" { + t.Fatalf("NormalizeVersion returned %q", version) + } +} + +func TestControllerCheckAndDownload(t *testing.T) { + t.Parallel() + + platform := &fakePlatform{ + root: InstallRoot{Path: t.TempDir()}, + stage: StagedArtifact{ + Path: t.TempDir(), + Root: InstallRoot{Path: t.TempDir()}, + Bundle: BundleManifest{ + SchemaVersion: 1, + EntryPoint: "App", + Files: []BundleFile{{Path: "App", Mode: "0755"}}, + }, + }, + } + release := &Release{ + ID: "1.1.0", + Version: "1.1.0", + Channel: ChannelStable, + Artifact: Artifact{ + Kind: ArtifactKindBundleArchive, + Format: ArtifactFormatZip, + URL: "https://example.invalid/app.zip", + SHA256: "deadbeef", + }, + } + + controller, err := NewController(Config{ + App: AppDescriptor{ + ProductID: "com.example.app", + CurrentVersion: "1.0.0", + Channel: ChannelStable, + OS: "linux", + Arch: "amd64", + ExecutablePath: filepath.Join(t.TempDir(), "App"), + }, + Provider: fakeProvider{release: release}, + Downloader: fakeDownloader{file: DownloadedFile{Path: "bundle.zip", SHA256: "deadbeef"}}, + Store: &memoryStore{}, + Platform: platform, + }) + if err != nil { + t.Fatalf("NewController returned error: %v", err) + } + + snapshot, err := controller.Check(context.Background(), CheckRequest{}) + if err != nil { + t.Fatalf("Check returned error: %v", err) + } + if snapshot.State != StateUpdateAvailable { + t.Fatalf("Check state = %s", snapshot.State) + } + + snapshot, err = controller.Download(context.Background()) + if err != nil { + t.Fatalf("Download returned error: %v", err) + } + if snapshot.State != StateReadyToApply { + t.Fatalf("Download state = %s", snapshot.State) + } + if snapshot.Staged == nil { + t.Fatal("Download did not stage artifact") + } +} + +func TestControllerBusy(t *testing.T) { + t.Parallel() + + block := make(chan struct{}) + controller, err := NewController(Config{ + App: AppDescriptor{ + ProductID: "com.example.app", + CurrentVersion: "1.0.0", + Channel: ChannelStable, + OS: "linux", + Arch: "amd64", + ExecutablePath: filepath.Join(t.TempDir(), "App"), + }, + Provider: fakeProvider{ + resolveFunc: func(context.Context, ResolveRequest) (*Release, error) { + <-block + return nil, nil + }, + }, + Downloader: fakeDownloader{}, + Store: &memoryStore{}, + Platform: &fakePlatform{root: InstallRoot{Path: t.TempDir()}}, + }) + if err != nil { + t.Fatalf("NewController returned error: %v", err) + } + + done := make(chan error, 1) + go func() { + _, err := controller.Check(context.Background(), CheckRequest{}) + done <- err + }() + + time.Sleep(50 * time.Millisecond) + if _, err := controller.Check(context.Background(), CheckRequest{}); !errors.Is(err, ErrBusy) { + t.Fatalf("expected ErrBusy, got %v", err) + } + close(block) + if err := <-done; err != nil { + t.Fatalf("first Check returned error: %v", err) + } +} + +func TestControllerCleansStagedUpdateOnMatchingVersion(t *testing.T) { + t.Parallel() + + store := &memoryStore{} + err := store.Save(context.Background(), Snapshot{ + State: StateRestarting, + Staged: &StagedArtifact{ + Release: Release{Version: "1.2.0"}, + }, + Candidate: &Release{Version: "1.2.0"}, + }) + if err != nil { + t.Fatalf("Save returned error: %v", err) + } + + controller, err := NewController(Config{ + App: AppDescriptor{ + ProductID: "com.example.app", + CurrentVersion: "1.2.0", + Channel: ChannelStable, + OS: "linux", + Arch: "amd64", + ExecutablePath: filepath.Join(t.TempDir(), "App"), + }, + Provider: fakeProvider{}, + Downloader: fakeDownloader{}, + Store: store, + Platform: &fakePlatform{root: InstallRoot{Path: t.TempDir()}}, + }) + if err != nil { + t.Fatalf("NewController returned error: %v", err) + } + + snapshot := controller.Snapshot() + if snapshot.Staged != nil { + t.Fatal("expected staged update to be cleared") + } + if snapshot.State != StateUpToDate { + t.Fatalf("snapshot state = %s", snapshot.State) + } +} + +type fakeProvider struct { + release *Release + resolveFunc func(context.Context, ResolveRequest) (*Release, error) +} + +func (provider fakeProvider) Resolve(ctx context.Context, req ResolveRequest) (*Release, error) { + if provider.resolveFunc != nil { + return provider.resolveFunc(ctx, req) + } + return provider.release, nil +} + +func (provider fakeProvider) OpenArtifact(context.Context, Release) (io.ReadCloser, error) { + return nil, nil +} + +type fakeDownloader struct { + file DownloadedFile + err error +} + +func (downloader fakeDownloader) Download(context.Context, Artifact) (DownloadedFile, error) { + return downloader.file, downloader.err +} + +type fakePlatform struct { + root InstallRoot + stage StagedArtifact + err error +} + +func (platform *fakePlatform) DetectInstallRoot(AppDescriptor) (InstallRoot, error) { + return platform.root, platform.err +} + +func (platform *fakePlatform) Stage(context.Context, InstallRoot, DownloadedFile, Release) (StagedArtifact, error) { + return platform.stage, platform.err +} + +func (platform *fakePlatform) SpawnApplyAndRestart(context.Context, ApplyRequest) error { + return platform.err +} + +type memoryStore struct { + mu sync.Mutex + snapshot Snapshot +} + +func (store *memoryStore) Load(context.Context) (Snapshot, error) { + store.mu.Lock() + defer store.mu.Unlock() + return store.snapshot, nil +} + +func (store *memoryStore) Save(_ context.Context, snapshot Snapshot) error { + store.mu.Lock() + defer store.mu.Unlock() + store.snapshot = snapshot + return nil +} + +func (store *memoryStore) ClearStaged(context.Context) error { + store.mu.Lock() + defer store.mu.Unlock() + store.snapshot.Staged = nil + store.snapshot.Candidate = nil + return nil +} diff --git a/pkg/wails3kit/updates/downloader.go b/pkg/wails3kit/updates/downloader.go new file mode 100644 index 0000000..9d69aff --- /dev/null +++ b/pkg/wails3kit/updates/downloader.go @@ -0,0 +1,58 @@ +package updates + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "net/http" + "os" +) + +type HTTPDownloader struct { + client *http.Client +} + +func NewHTTPDownloader(client *http.Client) *HTTPDownloader { + if client == nil { + client = http.DefaultClient + } + return &HTTPDownloader{client: client} +} + +func (downloader *HTTPDownloader) Download(ctx context.Context, artifact Artifact) (DownloadedFile, error) { + request, err := http.NewRequestWithContext(ctx, http.MethodGet, artifact.URL, nil) + if err != nil { + return DownloadedFile{}, err + } + + response, err := downloader.client.Do(request) + if err != nil { + return DownloadedFile{}, err + } + defer response.Body.Close() + + if response.StatusCode < 200 || response.StatusCode >= 300 { + return DownloadedFile{}, fmt.Errorf("unexpected download status: %s", response.Status) + } + + file, err := os.CreateTemp("", "wails3kit-download-*") + if err != nil { + return DownloadedFile{}, err + } + defer file.Close() + + hash := sha256.New() + written, err := io.Copy(io.MultiWriter(file, hash), response.Body) + if err != nil { + _ = os.Remove(file.Name()) + return DownloadedFile{}, err + } + + return DownloadedFile{ + Path: file.Name(), + Size: written, + SHA256: hex.EncodeToString(hash.Sum(nil)), + }, nil +} diff --git a/pkg/wails3kit/updates/errors.go b/pkg/wails3kit/updates/errors.go new file mode 100644 index 0000000..e1d2a1a --- /dev/null +++ b/pkg/wails3kit/updates/errors.go @@ -0,0 +1,13 @@ +package updates + +import "errors" + +var ( + ErrBusy = errors.New("update operation already in progress") + ErrNoUpdate = errors.New("no update available") + ErrNoStagedUpdate = errors.New("no staged update available") + ErrInvalidConfig = errors.New("invalid update configuration") + ErrInvalidVersion = errors.New("invalid semantic version") + ErrInvalidArtifact = errors.New("invalid artifact") + ErrChecksumMismatch = errors.New("artifact checksum mismatch") +) diff --git a/pkg/wails3kit/updates/platform/BUILD.bazel b/pkg/wails3kit/updates/platform/BUILD.bazel new file mode 100644 index 0000000..a191cff --- /dev/null +++ b/pkg/wails3kit/updates/platform/BUILD.bazel @@ -0,0 +1,22 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "platform", + srcs = [ + "apply.go", + "archive.go", + "installer.go", + ], + importpath = "github.com/Eriyc/rules_wails/pkg/wails3kit/updates/platform", + visibility = ["//visibility:public"], + deps = [ + "//pkg/wails3kit/updates", + ], +) + +go_test( + name = "platform_test", + srcs = ["platform_test.go"], + embed = [":platform"], + deps = ["//pkg/wails3kit/updates"], +) diff --git a/pkg/wails3kit/updates/platform/apply.go b/pkg/wails3kit/updates/platform/apply.go new file mode 100644 index 0000000..f91dc21 --- /dev/null +++ b/pkg/wails3kit/updates/platform/apply.go @@ -0,0 +1,119 @@ +package platform + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strconv" + + "github.com/Eriyc/rules_wails/pkg/wails3kit/updates" +) + +type restorePoint struct { + path string + backup string + hadPrior bool +} + +func applyBundle(request helperRequest) error { + backupDir, err := os.MkdirTemp("", "wails3kit-backup-*") + if err != nil { + return err + } + + restores := make([]restorePoint, 0, len(request.Bundle.Files)) + for _, file := range request.Bundle.Files { + source := filepath.Join(request.StagedPath, filepath.FromSlash(file.Path)) + target := filepath.Join(request.InstallRoot, filepath.FromSlash(file.Path)) + modeValue, err := strconv.ParseUint(file.Mode, 8, 32) + if err != nil { + rollback(restores) + return err + } + mode := os.FileMode(modeValue) + + restore, err := backupTarget(backupDir, target) + if err != nil { + rollback(restores) + return err + } + restores = append(restores, restore) + + if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { + rollback(restores) + return err + } + if err := os.RemoveAll(target); err != nil { + rollback(restores) + return err + } + if err := copyFile(source, target, mode); err != nil { + rollback(restores) + return err + } + } + + return nil +} + +func backupTarget(backupDir string, target string) (restorePoint, error) { + point := restorePoint{path: target} + info, err := os.Stat(target) + if err != nil { + if os.IsNotExist(err) { + return point, nil + } + return point, err + } + if info.IsDir() { + return point, fmt.Errorf("%w: target %s is a directory", updates.ErrInvalidArtifact, target) + } + + backupPath := filepath.Join(backupDir, filepath.Base(target)) + if err := copyFile(target, backupPath, info.Mode()); err != nil { + return point, err + } + point.hadPrior = true + point.backup = backupPath + return point, nil +} + +func rollback(restores []restorePoint) { + for index := len(restores) - 1; index >= 0; index-- { + restore := restores[index] + if restore.hadPrior { + info, err := os.Stat(restore.backup) + if err == nil { + _ = os.RemoveAll(restore.path) + _ = os.MkdirAll(filepath.Dir(restore.path), 0o755) + _ = copyFile(restore.backup, restore.path, info.Mode()) + } + continue + } + _ = os.RemoveAll(restore.path) + } +} + +func copyFile(sourcePath string, destinationPath string, mode os.FileMode) error { + source, err := os.Open(sourcePath) + if err != nil { + return err + } + defer source.Close() + + return writeFile(destinationPath, source, mode) +} + +func writeFile(path string, source io.Reader, mode os.FileMode) error { + file, err := os.Create(path) + if err != nil { + return err + } + defer file.Close() + + if _, err := io.Copy(file, source); err != nil { + return err + } + return file.Chmod(mode) +} diff --git a/pkg/wails3kit/updates/platform/archive.go b/pkg/wails3kit/updates/platform/archive.go new file mode 100644 index 0000000..3722531 --- /dev/null +++ b/pkg/wails3kit/updates/platform/archive.go @@ -0,0 +1,188 @@ +package platform + +import ( + "archive/tar" + "archive/zip" + "compress/gzip" + "context" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/Eriyc/rules_wails/pkg/wails3kit/updates" +) + +func extractBundle(_ context.Context, archivePath string, format updates.ArtifactFormat) (string, updates.BundleManifest, error) { + stageDir, err := os.MkdirTemp("", "wails3kit-stage-*") + if err != nil { + return "", updates.BundleManifest{}, err + } + + switch format { + case updates.ArtifactFormatZip: + err = extractZip(archivePath, stageDir) + case updates.ArtifactFormatTarGz: + err = extractTarGz(archivePath, stageDir) + default: + err = fmt.Errorf("%w: unsupported format %s", updates.ErrInvalidArtifact, format) + } + if err != nil { + _ = os.RemoveAll(stageDir) + return "", updates.BundleManifest{}, err + } + + manifestPath := filepath.Join(stageDir, "bundle.json") + bytes, err := os.ReadFile(manifestPath) + if err != nil { + _ = os.RemoveAll(stageDir) + return "", updates.BundleManifest{}, err + } + + var manifest updates.BundleManifest + if err := json.Unmarshal(bytes, &manifest); err != nil { + _ = os.RemoveAll(stageDir) + return "", updates.BundleManifest{}, err + } + + if err := validateBundle(stageDir, manifest); err != nil { + _ = os.RemoveAll(stageDir) + return "", updates.BundleManifest{}, err + } + + return stageDir, manifest, nil +} + +func extractZip(archivePath string, targetDir string) error { + reader, err := zip.OpenReader(archivePath) + if err != nil { + return err + } + defer reader.Close() + + for _, file := range reader.File { + path, err := safeArchivePath(targetDir, file.Name) + if err != nil { + return err + } + if file.FileInfo().IsDir() { + if err := os.MkdirAll(path, 0o755); err != nil { + return err + } + continue + } + + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + + source, err := file.Open() + if err != nil { + return err + } + if err := writeFile(path, source, file.Mode()); err != nil { + _ = source.Close() + return err + } + _ = source.Close() + } + + return nil +} + +func extractTarGz(archivePath string, targetDir string) error { + file, err := os.Open(archivePath) + if err != nil { + return err + } + defer file.Close() + + gzipReader, err := gzip.NewReader(file) + if err != nil { + return err + } + defer gzipReader.Close() + + reader := tar.NewReader(gzipReader) + for { + header, err := reader.Next() + if err == io.EOF { + return nil + } + if err != nil { + return err + } + + path, err := safeArchivePath(targetDir, header.Name) + if err != nil { + return err + } + + switch header.Typeflag { + case tar.TypeDir: + if err := os.MkdirAll(path, 0o755); err != nil { + return err + } + case tar.TypeReg: + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + if err := writeFile(path, reader, os.FileMode(header.Mode)); err != nil { + return err + } + default: + return fmt.Errorf("%w: unsupported tar entry %s", updates.ErrInvalidArtifact, header.Name) + } + } +} + +func validateBundle(stageDir string, manifest updates.BundleManifest) error { + if manifest.SchemaVersion != 1 { + return fmt.Errorf("%w: unsupported bundle schema %d", updates.ErrInvalidArtifact, manifest.SchemaVersion) + } + if !isSafeRelative(manifest.EntryPoint) { + return fmt.Errorf("%w: invalid entrypoint", updates.ErrInvalidArtifact) + } + if len(manifest.Files) == 0 { + return fmt.Errorf("%w: bundle contains no files", updates.ErrInvalidArtifact) + } + + for _, file := range manifest.Files { + if !isSafeRelative(file.Path) { + return fmt.Errorf("%w: invalid bundle path %s", updates.ErrInvalidArtifact, file.Path) + } + if _, err := strconv.ParseUint(file.Mode, 8, 32); err != nil { + return fmt.Errorf("%w: invalid mode %s", updates.ErrInvalidArtifact, file.Mode) + } + info, err := os.Stat(filepath.Join(stageDir, filepath.FromSlash(file.Path))) + if err != nil { + return err + } + if info.IsDir() { + return fmt.Errorf("%w: file entry %s is a directory", updates.ErrInvalidArtifact, file.Path) + } + } + + return nil +} + +func safeArchivePath(root string, name string) (string, error) { + if !isSafeRelative(name) && strings.TrimSpace(name) != "bundle.json" { + return "", fmt.Errorf("%w: unsafe archive path %s", updates.ErrInvalidArtifact, name) + } + return filepath.Join(root, filepath.FromSlash(name)), nil +} + +func isSafeRelative(path string) bool { + cleaned := filepath.Clean(filepath.FromSlash(path)) + if cleaned == "." || cleaned == "" { + return false + } + if filepath.IsAbs(cleaned) { + return false + } + return !strings.HasPrefix(cleaned, "..") +} diff --git a/pkg/wails3kit/updates/platform/darwin/BUILD.bazel b/pkg/wails3kit/updates/platform/darwin/BUILD.bazel new file mode 100644 index 0000000..e0eb1b2 --- /dev/null +++ b/pkg/wails3kit/updates/platform/darwin/BUILD.bazel @@ -0,0 +1,12 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "darwin", + srcs = ["darwin.go"], + importpath = "github.com/Eriyc/rules_wails/pkg/wails3kit/updates/platform/darwin", + visibility = ["//visibility:public"], + deps = [ + "//pkg/wails3kit/updates", + "//pkg/wails3kit/updates/platform", + ], +) diff --git a/pkg/wails3kit/updates/platform/darwin/darwin.go b/pkg/wails3kit/updates/platform/darwin/darwin.go new file mode 100644 index 0000000..af14c5c --- /dev/null +++ b/pkg/wails3kit/updates/platform/darwin/darwin.go @@ -0,0 +1,31 @@ +package darwin + +import ( + "errors" + "path/filepath" + + "github.com/Eriyc/rules_wails/pkg/wails3kit/updates" + "github.com/Eriyc/rules_wails/pkg/wails3kit/updates/platform" +) + +func New() updates.PlatformInstaller { + return platform.New(DetectInstallRoot) +} + +func DetectInstallRoot(app updates.AppDescriptor) (updates.InstallRoot, error) { + if app.ExecutablePath == "" { + return updates.InstallRoot{}, errors.New("missing executable path") + } + + current := filepath.Dir(app.ExecutablePath) + for { + if filepath.Ext(current) == ".app" { + return updates.InstallRoot{Path: current}, nil + } + parent := filepath.Dir(current) + if parent == current { + return updates.InstallRoot{}, errors.New("unable to locate .app bundle") + } + current = parent + } +} diff --git a/pkg/wails3kit/updates/platform/installer.go b/pkg/wails3kit/updates/platform/installer.go new file mode 100644 index 0000000..19eb07c --- /dev/null +++ b/pkg/wails3kit/updates/platform/installer.go @@ -0,0 +1,172 @@ +package platform + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "time" + + "github.com/Eriyc/rules_wails/pkg/wails3kit/updates" +) + +const helperFlag = "--wails3kit-update-helper" + +type Detector func(app updates.AppDescriptor) (updates.InstallRoot, error) + +type Installer struct { + detector Detector +} + +func New(detector Detector) *Installer { + return &Installer{detector: detector} +} + +func (installer *Installer) DetectInstallRoot(app updates.AppDescriptor) (updates.InstallRoot, error) { + if installer.detector == nil { + return updates.InstallRoot{}, errors.New("missing install root detector") + } + return installer.detector(app) +} + +func (installer *Installer) Stage(ctx context.Context, root updates.InstallRoot, file updates.DownloadedFile, release updates.Release) (updates.StagedArtifact, error) { + stageDir, bundleManifest, err := extractBundle(ctx, file.Path, release.Artifact.Format) + if err != nil { + return updates.StagedArtifact{}, err + } + + return updates.StagedArtifact{ + Path: stageDir, + Root: root, + Release: release, + Bundle: bundleManifest, + }, nil +} + +func (installer *Installer) SpawnApplyAndRestart(_ context.Context, req updates.ApplyRequest) error { + helperDir, err := os.MkdirTemp("", "wails3kit-helper-*") + if err != nil { + return err + } + + helperBinary := filepath.Join(helperDir, filepath.Base(req.App.ExecutablePath)) + if err := copyFile(req.App.ExecutablePath, helperBinary, 0o755); err != nil { + return err + } + + payload := helperRequest{ + InstallRoot: req.Root.Path, + StagedPath: req.Staged.Path, + Bundle: req.Staged.Bundle, + Args: append([]string(nil), req.App.Args...), + WorkingDirectory: req.App.WorkingDirectory, + } + + requestPath := filepath.Join(helperDir, "request.json") + encoded, err := json.Marshal(payload) + if err != nil { + return err + } + if err := os.WriteFile(requestPath, encoded, 0o600); err != nil { + return err + } + + command := exec.Command(helperBinary, helperFlag, requestPath) + command.Dir = helperDir + command.Stdout = os.Stdout + command.Stderr = os.Stderr + return command.Start() +} + +func MaybeRun(args []string) (bool, error) { + if len(args) < 3 || args[1] != helperFlag { + return false, nil + } + return true, runHelper(args[2]) +} + +func NewCurrent() (*Installer, error) { + switch runtime.GOOS { + case "darwin": + return New(detectDarwinInstallRoot), nil + case "windows": + return New(detectDirectoryInstallRoot), nil + case "linux": + return New(detectDirectoryInstallRoot), nil + default: + return nil, fmt.Errorf("unsupported platform: %s", runtime.GOOS) + } +} + +type helperRequest struct { + InstallRoot string `json:"installRoot"` + StagedPath string `json:"stagedPath"` + Bundle updates.BundleManifest `json:"bundle"` + Args []string `json:"args,omitempty"` + WorkingDirectory string `json:"workingDirectory,omitempty"` +} + +func runHelper(requestPath string) error { + bytes, err := os.ReadFile(requestPath) + if err != nil { + return err + } + + var request helperRequest + if err := json.Unmarshal(bytes, &request); err != nil { + return err + } + + deadline := time.Now().Add(15 * time.Second) + var applyErr error + for time.Now().Before(deadline) { + applyErr = applyBundle(request) + if applyErr == nil { + break + } + time.Sleep(100 * time.Millisecond) + } + if applyErr != nil { + return applyErr + } + + relaunch := filepath.Join(request.InstallRoot, filepath.FromSlash(request.Bundle.EntryPoint)) + command := exec.Command(relaunch, request.Args...) + if request.WorkingDirectory != "" { + command.Dir = request.WorkingDirectory + } else { + command.Dir = request.InstallRoot + } + command.Stdout = os.Stdout + command.Stderr = os.Stderr + return command.Start() +} + +func detectDirectoryInstallRoot(app updates.AppDescriptor) (updates.InstallRoot, error) { + if app.ExecutablePath == "" { + return updates.InstallRoot{}, errors.New("missing executable path") + } + return updates.InstallRoot{Path: filepath.Dir(app.ExecutablePath)}, nil +} + +func detectDarwinInstallRoot(app updates.AppDescriptor) (updates.InstallRoot, error) { + if app.ExecutablePath == "" { + return updates.InstallRoot{}, errors.New("missing executable path") + } + + current := filepath.Dir(app.ExecutablePath) + for { + if filepath.Ext(current) == ".app" { + return updates.InstallRoot{Path: current}, nil + } + parent := filepath.Dir(current) + if parent == current { + return updates.InstallRoot{}, fmt.Errorf("unable to locate .app bundle from %s", app.ExecutablePath) + } + current = parent + } +} diff --git a/pkg/wails3kit/updates/platform/linux/BUILD.bazel b/pkg/wails3kit/updates/platform/linux/BUILD.bazel new file mode 100644 index 0000000..e835e6e --- /dev/null +++ b/pkg/wails3kit/updates/platform/linux/BUILD.bazel @@ -0,0 +1,12 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "linux", + srcs = ["linux.go"], + importpath = "github.com/Eriyc/rules_wails/pkg/wails3kit/updates/platform/linux", + visibility = ["//visibility:public"], + deps = [ + "//pkg/wails3kit/updates", + "//pkg/wails3kit/updates/platform", + ], +) diff --git a/pkg/wails3kit/updates/platform/linux/linux.go b/pkg/wails3kit/updates/platform/linux/linux.go new file mode 100644 index 0000000..9384dcf --- /dev/null +++ b/pkg/wails3kit/updates/platform/linux/linux.go @@ -0,0 +1,20 @@ +package linux + +import ( + "errors" + "path/filepath" + + "github.com/Eriyc/rules_wails/pkg/wails3kit/updates" + "github.com/Eriyc/rules_wails/pkg/wails3kit/updates/platform" +) + +func New() updates.PlatformInstaller { + return platform.New(DetectInstallRoot) +} + +func DetectInstallRoot(app updates.AppDescriptor) (updates.InstallRoot, error) { + if app.ExecutablePath == "" { + return updates.InstallRoot{}, errors.New("missing executable path") + } + return updates.InstallRoot{Path: filepath.Dir(app.ExecutablePath)}, nil +} diff --git a/pkg/wails3kit/updates/platform/platform_test.go b/pkg/wails3kit/updates/platform/platform_test.go new file mode 100644 index 0000000..59fa407 --- /dev/null +++ b/pkg/wails3kit/updates/platform/platform_test.go @@ -0,0 +1,134 @@ +package platform + +import ( + "archive/zip" + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/Eriyc/rules_wails/pkg/wails3kit/updates" +) + +func TestExtractBundleRejectsTraversal(t *testing.T) { + t.Parallel() + + archivePath := filepath.Join(t.TempDir(), "bundle.zip") + file, err := os.Create(archivePath) + if err != nil { + t.Fatalf("Create returned error: %v", err) + } + writer := zip.NewWriter(file) + entry, err := writer.Create("../escape") + if err != nil { + t.Fatalf("Create entry returned error: %v", err) + } + if _, err := entry.Write([]byte("bad")); err != nil { + t.Fatalf("Write returned error: %v", err) + } + if err := writer.Close(); err != nil { + t.Fatalf("Close writer returned error: %v", err) + } + if err := file.Close(); err != nil { + t.Fatalf("Close file returned error: %v", err) + } + + if _, _, err := extractBundle(t.Context(), archivePath, updates.ArtifactFormatZip); err == nil { + t.Fatal("expected traversal archive to fail") + } +} + +func TestApplyBundleReplacesFilesAndPreservesArgs(t *testing.T) { + t.Parallel() + + root := t.TempDir() + stage := t.TempDir() + if err := os.MkdirAll(filepath.Join(root, "resources"), 0o755); err != nil { + t.Fatalf("MkdirAll returned error: %v", err) + } + if err := os.WriteFile(filepath.Join(root, "App"), []byte("old"), 0o755); err != nil { + t.Fatalf("WriteFile returned error: %v", err) + } + if err := os.WriteFile(filepath.Join(root, "resources", "config.json"), []byte("old"), 0o644); err != nil { + t.Fatalf("WriteFile returned error: %v", err) + } + if err := os.MkdirAll(filepath.Join(stage, "resources"), 0o755); err != nil { + t.Fatalf("MkdirAll returned error: %v", err) + } + if err := os.WriteFile(filepath.Join(stage, "App"), []byte("new"), 0o755); err != nil { + t.Fatalf("WriteFile returned error: %v", err) + } + if err := os.WriteFile(filepath.Join(stage, "resources", "config.json"), []byte("new"), 0o644); err != nil { + t.Fatalf("WriteFile returned error: %v", err) + } + + request := helperRequest{ + InstallRoot: root, + StagedPath: stage, + Bundle: updates.BundleManifest{ + SchemaVersion: 1, + EntryPoint: "App", + Files: []updates.BundleFile{ + {Path: "App", Mode: "0755"}, + {Path: "resources/config.json", Mode: "0644"}, + }, + }, + } + + if err := applyBundle(request); err != nil { + t.Fatalf("applyBundle returned error: %v", err) + } + + bytes, err := os.ReadFile(filepath.Join(root, "App")) + if err != nil { + t.Fatalf("ReadFile returned error: %v", err) + } + if string(bytes) != "new" { + t.Fatalf("unexpected executable contents: %s", string(bytes)) + } + bytes, err = os.ReadFile(filepath.Join(root, "resources", "config.json")) + if err != nil { + t.Fatalf("ReadFile returned error: %v", err) + } + if string(bytes) != "new" { + t.Fatalf("unexpected resource contents: %s", string(bytes)) + } +} + +func TestMaybeRunIgnoresNormalArgs(t *testing.T) { + t.Parallel() + + handled, err := MaybeRun([]string{"app"}) + if err != nil { + t.Fatalf("MaybeRun returned error: %v", err) + } + if handled { + t.Fatal("expected MaybeRun to ignore non-helper args") + } +} + +func TestHelperRequestJSONRoundTrip(t *testing.T) { + t.Parallel() + + request := helperRequest{ + InstallRoot: "/tmp/app", + StagedPath: "/tmp/stage", + Bundle: updates.BundleManifest{ + SchemaVersion: 1, + EntryPoint: "App", + Files: []updates.BundleFile{{Path: "App", Mode: "0755"}}, + }, + Args: []string{"--flag"}, + } + bytes, err := json.Marshal(request) + if err != nil { + t.Fatalf("Marshal returned error: %v", err) + } + var decoded helperRequest + if err := json.Unmarshal(bytes, &decoded); err != nil { + t.Fatalf("Unmarshal returned error: %v", err) + } + if decoded.Bundle.EntryPoint != "App" { + t.Fatalf("unexpected entrypoint: %s", decoded.Bundle.EntryPoint) + } +} diff --git a/pkg/wails3kit/updates/platform/windows/BUILD.bazel b/pkg/wails3kit/updates/platform/windows/BUILD.bazel new file mode 100644 index 0000000..b006412 --- /dev/null +++ b/pkg/wails3kit/updates/platform/windows/BUILD.bazel @@ -0,0 +1,12 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "windows", + srcs = ["windows.go"], + importpath = "github.com/Eriyc/rules_wails/pkg/wails3kit/updates/platform/windows", + visibility = ["//visibility:public"], + deps = [ + "//pkg/wails3kit/updates", + "//pkg/wails3kit/updates/platform", + ], +) diff --git a/pkg/wails3kit/updates/platform/windows/windows.go b/pkg/wails3kit/updates/platform/windows/windows.go new file mode 100644 index 0000000..ed06cf3 --- /dev/null +++ b/pkg/wails3kit/updates/platform/windows/windows.go @@ -0,0 +1,20 @@ +package windows + +import ( + "errors" + "path/filepath" + + "github.com/Eriyc/rules_wails/pkg/wails3kit/updates" + "github.com/Eriyc/rules_wails/pkg/wails3kit/updates/platform" +) + +func New() updates.PlatformInstaller { + return platform.New(DetectInstallRoot) +} + +func DetectInstallRoot(app updates.AppDescriptor) (updates.InstallRoot, error) { + if app.ExecutablePath == "" { + return updates.InstallRoot{}, errors.New("missing executable path") + } + return updates.InstallRoot{Path: filepath.Dir(app.ExecutablePath)}, nil +} diff --git a/pkg/wails3kit/updates/providers/githubreleases/BUILD.bazel b/pkg/wails3kit/updates/providers/githubreleases/BUILD.bazel new file mode 100644 index 0000000..fab3ee2 --- /dev/null +++ b/pkg/wails3kit/updates/providers/githubreleases/BUILD.bazel @@ -0,0 +1,16 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "githubreleases", + srcs = ["provider.go"], + importpath = "github.com/Eriyc/rules_wails/pkg/wails3kit/updates/providers/githubreleases", + visibility = ["//visibility:public"], + deps = ["//pkg/wails3kit/updates"], +) + +go_test( + name = "githubreleases_test", + srcs = ["provider_test.go"], + embed = [":githubreleases"], + deps = ["//pkg/wails3kit/updates"], +) diff --git a/pkg/wails3kit/updates/providers/githubreleases/provider.go b/pkg/wails3kit/updates/providers/githubreleases/provider.go new file mode 100644 index 0000000..59d4e43 --- /dev/null +++ b/pkg/wails3kit/updates/providers/githubreleases/provider.go @@ -0,0 +1,259 @@ +package githubreleases + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "text/template" + "time" + + "github.com/Eriyc/rules_wails/pkg/wails3kit/updates" +) + +const ( + defaultAssetTemplate = "{{ .ProductID }}_{{ .Version }}_{{ .OS }}_{{ .Arch }}.zip" + defaultChecksumAssetTemplate = "{{ .AssetName }}.sha256" +) + +type Config struct { + Owner string + Repo string + HTTPClient *http.Client + PrepareRequest func(*http.Request) error + AssetNameTemplate string + ChecksumAssetNameTemplate string +} + +type Provider struct { + config Config + client *http.Client + assetNameTemplate *template.Template + checksumNameTemplate *template.Template + baseURL string +} + +func New(cfg Config) (*Provider, error) { + if cfg.Owner == "" || cfg.Repo == "" { + return nil, updates.ErrInvalidConfig + } + client := cfg.HTTPClient + if client == nil { + client = http.DefaultClient + } + assetTemplate := cfg.AssetNameTemplate + if assetTemplate == "" { + assetTemplate = defaultAssetTemplate + } + checksumTemplate := cfg.ChecksumAssetNameTemplate + if checksumTemplate == "" { + checksumTemplate = defaultChecksumAssetTemplate + } + + assetNameTemplate, err := template.New("asset").Parse(assetTemplate) + if err != nil { + return nil, err + } + checksumNameTemplate, err := template.New("checksum").Parse(checksumTemplate) + if err != nil { + return nil, err + } + + return &Provider{ + config: cfg, + client: client, + assetNameTemplate: assetNameTemplate, + checksumNameTemplate: checksumNameTemplate, + baseURL: "https://api.github.com", + }, nil +} + +type githubRelease struct { + TagName string `json:"tag_name"` + Body string `json:"body"` + Prerelease bool `json:"prerelease"` + PublishedAt time.Time `json:"published_at"` + Assets []githubAsset `json:"assets"` +} + +type githubAsset struct { + Name string `json:"name"` + BrowserDownloadURL string `json:"browser_download_url"` +} + +func (provider *Provider) Resolve(ctx context.Context, req updates.ResolveRequest) (*updates.Release, error) { + request, err := http.NewRequestWithContext(ctx, http.MethodGet, provider.releaseURL(), nil) + if err != nil { + return nil, err + } + if provider.config.PrepareRequest != nil { + if err := provider.config.PrepareRequest(request); err != nil { + return nil, err + } + } + + response, err := provider.client.Do(request) + if err != nil { + return nil, err + } + defer response.Body.Close() + + if response.StatusCode < 200 || response.StatusCode >= 300 { + return nil, fmt.Errorf("unexpected github status: %s", response.Status) + } + + var releases []githubRelease + if err := json.NewDecoder(response.Body).Decode(&releases); err != nil { + return nil, err + } + + var best *updates.Release + for _, release := range releases { + version := strings.TrimPrefix(release.TagName, "v") + channel := mapChannel(release) + if channel != req.App.Channel { + continue + } + comparison, err := updates.CompareVersions(version, req.App.CurrentVersion) + if err != nil || comparison <= 0 { + continue + } + + assetName, err := provider.renderAssetName(req.App, version, "") + if err != nil { + return nil, err + } + checksumName, err := provider.renderChecksumName(req.App, version, assetName) + if err != nil { + return nil, err + } + + artifactURL := "" + checksumURL := "" + for _, asset := range release.Assets { + if asset.Name == assetName { + artifactURL = asset.BrowserDownloadURL + } + if asset.Name == checksumName { + checksumURL = asset.BrowserDownloadURL + } + } + if artifactURL == "" || checksumURL == "" { + continue + } + + sha256Value, err := provider.readChecksum(ctx, checksumURL) + if err != nil { + return nil, err + } + + candidate := &updates.Release{ + ID: version, + Version: version, + Channel: channel, + NotesMarkdown: release.Body, + PublishedAt: release.PublishedAt, + Artifact: updates.Artifact{ + Kind: updates.ArtifactKindBundleArchive, + Format: updates.ArtifactFormatZip, + URL: artifactURL, + SHA256: sha256Value, + }, + } + if best == nil { + best = candidate + continue + } + better, err := updates.CompareVersions(candidate.Version, best.Version) + if err != nil { + return nil, err + } + if better > 0 { + best = candidate + } + } + + return best, nil +} + +func (provider *Provider) OpenArtifact(ctx context.Context, release updates.Release) (io.ReadCloser, error) { + request, err := http.NewRequestWithContext(ctx, http.MethodGet, release.Artifact.URL, nil) + if err != nil { + return nil, err + } + if provider.config.PrepareRequest != nil { + if err := provider.config.PrepareRequest(request); err != nil { + return nil, err + } + } + response, err := provider.client.Do(request) + if err != nil { + return nil, err + } + if response.StatusCode < 200 || response.StatusCode >= 300 { + defer response.Body.Close() + return nil, fmt.Errorf("unexpected artifact status: %s", response.Status) + } + return response.Body, nil +} + +func (provider *Provider) readChecksum(ctx context.Context, url string) (string, error) { + request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return "", err + } + if provider.config.PrepareRequest != nil { + if err := provider.config.PrepareRequest(request); err != nil { + return "", err + } + } + response, err := provider.client.Do(request) + if err != nil { + return "", err + } + defer response.Body.Close() + if response.StatusCode < 200 || response.StatusCode >= 300 { + return "", fmt.Errorf("unexpected checksum status: %s", response.Status) + } + bytes, err := io.ReadAll(response.Body) + if err != nil { + return "", err + } + return strings.Fields(string(bytes))[0], nil +} + +func (provider *Provider) releaseURL() string { + return fmt.Sprintf("%s/repos/%s/%s/releases", provider.baseURL, provider.config.Owner, provider.config.Repo) +} + +func (provider *Provider) renderAssetName(app updates.AppDescriptor, version string, assetName string) (string, error) { + return provider.executeTemplate(provider.assetNameTemplate, app, version, assetName) +} + +func (provider *Provider) renderChecksumName(app updates.AppDescriptor, version string, assetName string) (string, error) { + return provider.executeTemplate(provider.checksumNameTemplate, app, version, assetName) +} + +func (provider *Provider) executeTemplate(templateValue *template.Template, app updates.AppDescriptor, version string, assetName string) (string, error) { + var builder strings.Builder + err := templateValue.Execute(&builder, map[string]string{ + "ProductID": app.ProductID, + "Version": version, + "OS": app.OS, + "Arch": app.Arch, + "AssetName": assetName, + }) + return builder.String(), err +} + +func mapChannel(release githubRelease) updates.Channel { + if !release.Prerelease { + return updates.ChannelStable + } + if strings.Contains(strings.ToLower(release.TagName), "alpha") { + return updates.ChannelAlpha + } + return updates.ChannelBeta +} diff --git a/pkg/wails3kit/updates/providers/githubreleases/provider_test.go b/pkg/wails3kit/updates/providers/githubreleases/provider_test.go new file mode 100644 index 0000000..6033de0 --- /dev/null +++ b/pkg/wails3kit/updates/providers/githubreleases/provider_test.go @@ -0,0 +1,72 @@ +package githubreleases + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/Eriyc/rules_wails/pkg/wails3kit/updates" +) + +func TestResolveSelectsReleaseFromGitHubAPI(t *testing.T) { + t.Parallel() + + serverURL := "" + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + switch request.URL.Path { + case "/repos/owner/repo/releases": + _, _ = writer.Write([]byte(`[ + { + "tag_name": "v1.2.0", + "body": "notes", + "prerelease": false, + "published_at": "2026-03-01T03:10:56Z", + "assets": [ + {"name": "com.example.app_1.2.0_linux_amd64.zip", "browser_download_url": "` + serverURL + `/artifact.zip"}, + {"name": "com.example.app_1.2.0_linux_amd64.zip.sha256", "browser_download_url": "` + serverURL + `/artifact.zip.sha256"} + ] + } + ]`)) + case "/artifact.zip.sha256": + _, _ = writer.Write([]byte("abcdef artifact.zip")) + default: + http.NotFound(writer, request) + } + })) + defer server.Close() + serverURL = server.URL + + provider, err := New(Config{ + Owner: "owner", + Repo: "repo", + HTTPClient: server.Client(), + }) + if err != nil { + t.Fatalf("New returned error: %v", err) + } + + provider.baseURL = server.URL + + release, err := provider.Resolve(context.Background(), updates.ResolveRequest{ + App: updates.AppDescriptor{ + ProductID: "com.example.app", + CurrentVersion: "1.0.0", + Channel: updates.ChannelStable, + OS: "linux", + Arch: "amd64", + }, + }) + if err != nil { + t.Fatalf("Resolve returned error: %v", err) + } + if release == nil { + t.Fatal("Resolve returned nil release") + } + if release.Artifact.URL != server.URL+"/artifact.zip" { + t.Fatalf("unexpected artifact url: %s", release.Artifact.URL) + } + if release.Artifact.SHA256 != "abcdef" { + t.Fatalf("unexpected checksum: %s", release.Artifact.SHA256) + } +} diff --git a/pkg/wails3kit/updates/providers/httpmanifest/BUILD.bazel b/pkg/wails3kit/updates/providers/httpmanifest/BUILD.bazel new file mode 100644 index 0000000..de06e9f --- /dev/null +++ b/pkg/wails3kit/updates/providers/httpmanifest/BUILD.bazel @@ -0,0 +1,19 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "httpmanifest", + srcs = [ + "provider.go", + "time.go", + ], + importpath = "github.com/Eriyc/rules_wails/pkg/wails3kit/updates/providers/httpmanifest", + visibility = ["//visibility:public"], + deps = ["//pkg/wails3kit/updates"], +) + +go_test( + name = "httpmanifest_test", + srcs = ["provider_test.go"], + embed = [":httpmanifest"], + deps = ["//pkg/wails3kit/updates"], +) diff --git a/pkg/wails3kit/updates/providers/httpmanifest/provider.go b/pkg/wails3kit/updates/providers/httpmanifest/provider.go new file mode 100644 index 0000000..31e7302 --- /dev/null +++ b/pkg/wails3kit/updates/providers/httpmanifest/provider.go @@ -0,0 +1,158 @@ +package httpmanifest + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/Eriyc/rules_wails/pkg/wails3kit/updates" +) + +type Config struct { + ManifestURL string + HTTPClient *http.Client + PrepareRequest func(*http.Request) error +} + +type Provider struct { + config Config + client *http.Client +} + +func New(cfg Config) (*Provider, error) { + if cfg.ManifestURL == "" { + return nil, updates.ErrInvalidConfig + } + client := cfg.HTTPClient + if client == nil { + client = http.DefaultClient + } + return &Provider{config: cfg, client: client}, nil +} + +type manifestDocument struct { + SchemaVersion int `json:"schemaVersion"` + ProductID string `json:"productID"` + Releases []manifestRelease `json:"releases"` +} + +type manifestRelease struct { + ID string `json:"id"` + Version string `json:"version"` + Channel updates.Channel `json:"channel"` + PublishedAt string `json:"publishedAt"` + NotesMarkdown string `json:"notesMarkdown"` + Artifacts []manifestArtifact `json:"artifacts"` +} + +type manifestArtifact struct { + OS string `json:"os"` + Arch string `json:"arch"` + Kind updates.ArtifactKind `json:"kind"` + Format updates.ArtifactFormat `json:"format"` + URL string `json:"url"` + SHA256 string `json:"sha256"` + Size int64 `json:"size"` +} + +func (provider *Provider) Resolve(ctx context.Context, req updates.ResolveRequest) (*updates.Release, error) { + request, err := http.NewRequestWithContext(ctx, http.MethodGet, provider.config.ManifestURL, nil) + if err != nil { + return nil, err + } + if provider.config.PrepareRequest != nil { + if err := provider.config.PrepareRequest(request); err != nil { + return nil, err + } + } + + response, err := provider.client.Do(request) + if err != nil { + return nil, err + } + defer response.Body.Close() + + if response.StatusCode < 200 || response.StatusCode >= 300 { + return nil, fmt.Errorf("unexpected manifest status: %s", response.Status) + } + + var document manifestDocument + if err := json.NewDecoder(response.Body).Decode(&document); err != nil { + return nil, err + } + + if document.ProductID != req.App.ProductID { + return nil, fmt.Errorf("manifest product mismatch: %s", document.ProductID) + } + + var best *updates.Release + for _, release := range document.Releases { + if release.Channel != req.App.Channel { + continue + } + comparison, err := updates.CompareVersions(release.Version, req.App.CurrentVersion) + if err != nil || comparison <= 0 { + continue + } + + for _, artifact := range release.Artifacts { + if artifact.OS != req.App.OS || artifact.Arch != req.App.Arch { + continue + } + parsedPublishedAt, err := parseReleaseTime(release.PublishedAt) + if err != nil { + return nil, err + } + candidate := &updates.Release{ + ID: release.ID, + Version: release.Version, + Channel: release.Channel, + NotesMarkdown: release.NotesMarkdown, + PublishedAt: parsedPublishedAt, + Artifact: updates.Artifact{ + Kind: artifact.Kind, + Format: artifact.Format, + URL: artifact.URL, + SHA256: artifact.SHA256, + Size: artifact.Size, + }, + } + if best == nil { + best = candidate + continue + } + better, err := updates.CompareVersions(candidate.Version, best.Version) + if err != nil { + return nil, err + } + if better > 0 { + best = candidate + } + } + } + + return best, nil +} + +func (provider *Provider) OpenArtifact(ctx context.Context, release updates.Release) (io.ReadCloser, error) { + request, err := http.NewRequestWithContext(ctx, http.MethodGet, release.Artifact.URL, nil) + if err != nil { + return nil, err + } + if provider.config.PrepareRequest != nil { + if err := provider.config.PrepareRequest(request); err != nil { + return nil, err + } + } + response, err := provider.client.Do(request) + if err != nil { + return nil, err + } + if response.StatusCode < 200 || response.StatusCode >= 300 { + defer response.Body.Close() + return nil, fmt.Errorf("unexpected artifact status: %s", response.Status) + } + return response.Body, nil +} diff --git a/pkg/wails3kit/updates/providers/httpmanifest/provider_test.go b/pkg/wails3kit/updates/providers/httpmanifest/provider_test.go new file mode 100644 index 0000000..216c48e --- /dev/null +++ b/pkg/wails3kit/updates/providers/httpmanifest/provider_test.go @@ -0,0 +1,73 @@ +package httpmanifest + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/Eriyc/rules_wails/pkg/wails3kit/updates" +) + +func TestResolveRequiresAuthAndSelectsNewest(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + if request.Header.Get("Authorization") != "Bearer token" { + http.Error(writer, "unauthorized", http.StatusUnauthorized) + return + } + _, _ = writer.Write([]byte(`{ + "schemaVersion": 1, + "productID": "com.example.app", + "releases": [ + { + "id": "1.1.0", + "version": "1.1.0", + "channel": "stable", + "publishedAt": "2026-03-01T03:10:56Z", + "artifacts": [ + {"os": "darwin", "arch": "arm64", "kind": "bundle-archive", "format": "zip", "url": "https://example.invalid/1.1.0.zip", "sha256": "111"} + ] + }, + { + "id": "1.2.0", + "version": "1.2.0", + "channel": "stable", + "publishedAt": "2026-03-02T03:10:56Z", + "artifacts": [ + {"os": "darwin", "arch": "arm64", "kind": "bundle-archive", "format": "zip", "url": "https://example.invalid/1.2.0.zip", "sha256": "222"} + ] + } + ] + }`)) + })) + defer server.Close() + + provider, err := New(Config{ + ManifestURL: server.URL, + PrepareRequest: func(request *http.Request) error { + request.Header.Set("Authorization", "Bearer token") + return nil + }, + }) + if err != nil { + t.Fatalf("New returned error: %v", err) + } + + release, err := provider.Resolve(context.Background(), updates.ResolveRequest{ + App: updates.AppDescriptor{ + ProductID: "com.example.app", + CurrentVersion: "1.0.0", + Channel: updates.ChannelStable, + OS: "darwin", + Arch: "arm64", + }, + }) + if err != nil { + t.Fatalf("Resolve returned error: %v", err) + } + if release == nil || release.Version != "1.2.0" { + t.Fatalf("Resolve selected %+v", release) + } +} diff --git a/pkg/wails3kit/updates/providers/httpmanifest/time.go b/pkg/wails3kit/updates/providers/httpmanifest/time.go new file mode 100644 index 0000000..21b01bd --- /dev/null +++ b/pkg/wails3kit/updates/providers/httpmanifest/time.go @@ -0,0 +1,7 @@ +package httpmanifest + +import "time" + +func parseReleaseTime(value string) (time.Time, error) { + return time.Parse(time.RFC3339, value) +} diff --git a/pkg/wails3kit/updates/semver.go b/pkg/wails3kit/updates/semver.go new file mode 100644 index 0000000..c99774a --- /dev/null +++ b/pkg/wails3kit/updates/semver.go @@ -0,0 +1,35 @@ +package updates + +import ( + "fmt" + "strings" + + "golang.org/x/mod/semver" +) + +func NormalizeVersion(version string) (string, error) { + trimmed := strings.TrimSpace(version) + if trimmed == "" { + return "", ErrInvalidVersion + } + if !strings.HasPrefix(trimmed, "v") { + trimmed = "v" + trimmed + } + if !semver.IsValid(trimmed) { + return "", fmt.Errorf("%w: %s", ErrInvalidVersion, version) + } + return trimmed, nil +} + +func CompareVersions(left string, right string) (int, error) { + normalizedLeft, err := NormalizeVersion(left) + if err != nil { + return 0, err + } + normalizedRight, err := NormalizeVersion(right) + if err != nil { + return 0, err + } + + return semver.Compare(normalizedLeft, normalizedRight), nil +} diff --git a/pkg/wails3kit/updates/storage/file/BUILD.bazel b/pkg/wails3kit/updates/storage/file/BUILD.bazel new file mode 100644 index 0000000..04dd068 --- /dev/null +++ b/pkg/wails3kit/updates/storage/file/BUILD.bazel @@ -0,0 +1,9 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "file", + srcs = ["store.go"], + importpath = "github.com/Eriyc/rules_wails/pkg/wails3kit/updates/storage/file", + visibility = ["//visibility:public"], + deps = ["//pkg/wails3kit/updates"], +) diff --git a/pkg/wails3kit/updates/storage/file/store.go b/pkg/wails3kit/updates/storage/file/store.go new file mode 100644 index 0000000..6ac3e58 --- /dev/null +++ b/pkg/wails3kit/updates/storage/file/store.go @@ -0,0 +1,91 @@ +package file + +import ( + "context" + "encoding/json" + "errors" + "os" + "path/filepath" + "sync" + + "github.com/Eriyc/rules_wails/pkg/wails3kit/updates" +) + +type Store struct { + path string + mu sync.Mutex +} + +func New(path string) *Store { + return &Store{path: path} +} + +func (store *Store) Path() string { + return store.path +} + +func (store *Store) Load(_ context.Context) (updates.Snapshot, error) { + store.mu.Lock() + defer store.mu.Unlock() + + bytes, err := os.ReadFile(store.path) + if errors.Is(err, os.ErrNotExist) { + return updates.Snapshot{}, nil + } + if err != nil { + return updates.Snapshot{}, err + } + + var snapshot updates.Snapshot + if err := json.Unmarshal(bytes, &snapshot); err != nil { + return updates.Snapshot{}, err + } + return snapshot, nil +} + +func (store *Store) Save(_ context.Context, snapshot updates.Snapshot) error { + store.mu.Lock() + defer store.mu.Unlock() + + if err := os.MkdirAll(filepath.Dir(store.path), 0o755); err != nil { + return err + } + + bytes, err := json.MarshalIndent(snapshot, "", " ") + if err != nil { + return err + } + + tempFile, err := os.CreateTemp(filepath.Dir(store.path), "snapshot-*.json") + if err != nil { + return err + } + tempPath := tempFile.Name() + defer func() { + _ = os.Remove(tempPath) + }() + + if _, err := tempFile.Write(bytes); err != nil { + _ = tempFile.Close() + return err + } + if err := tempFile.Close(); err != nil { + return err + } + + return os.Rename(tempPath, store.path) +} + +func (store *Store) ClearStaged(ctx context.Context) error { + snapshot, err := store.Load(ctx) + if err != nil { + return err + } + snapshot.Staged = nil + snapshot.Candidate = nil + snapshot.LastError = nil + if snapshot.State == updates.StateRestarting { + snapshot.State = updates.StateUpToDate + } + return store.Save(ctx, snapshot) +} diff --git a/pkg/wails3kit/updates/types.go b/pkg/wails3kit/updates/types.go new file mode 100644 index 0000000..1340c00 --- /dev/null +++ b/pkg/wails3kit/updates/types.go @@ -0,0 +1,157 @@ +package updates + +import ( + "context" + "io" + "log/slog" + "time" +) + +type Config struct { + App AppDescriptor + Provider ReleaseProvider + Downloader Downloader + Store SnapshotStore + Platform PlatformInstaller + Logger *slog.Logger + AutoCheckOnStartup bool +} + +type AppDescriptor struct { + ProductID string `json:"productID"` + CurrentVersion string `json:"currentVersion"` + Channel Channel `json:"channel"` + OS string `json:"os"` + Arch string `json:"arch"` + ExecutablePath string `json:"executablePath"` + Args []string `json:"args,omitempty"` + WorkingDirectory string `json:"workingDirectory,omitempty"` +} + +type Channel string + +const ( + ChannelStable Channel = "stable" + ChannelBeta Channel = "beta" + ChannelAlpha Channel = "alpha" +) + +type State string + +const ( + StateIdle State = "idle" + StateChecking State = "checking" + StateUpToDate State = "up_to_date" + StateUpdateAvailable State = "update_available" + StateDownloading State = "downloading" + StateDownloaded State = "downloaded" + StateVerifying State = "verifying" + StateReadyToApply State = "ready_to_apply" + StateApplying State = "applying" + StateRestarting State = "restarting" + StateFailed State = "failed" +) + +type Snapshot struct { + State State `json:"state"` + CurrentVersion string `json:"currentVersion"` + Channel Channel `json:"channel"` + LastCheckedAt *time.Time `json:"lastCheckedAt,omitempty"` + Candidate *Release `json:"candidate,omitempty"` + Staged *StagedArtifact `json:"staged,omitempty"` + LastError *ErrorInfo `json:"lastError,omitempty"` +} + +type Release struct { + ID string `json:"id"` + Version string `json:"version"` + Channel Channel `json:"channel"` + NotesMarkdown string `json:"notesMarkdown,omitempty"` + PublishedAt time.Time `json:"publishedAt"` + Artifact Artifact `json:"artifact"` +} + +type Artifact struct { + Kind ArtifactKind `json:"kind"` + Format ArtifactFormat `json:"format"` + URL string `json:"url"` + SHA256 string `json:"sha256"` + Size int64 `json:"size,omitempty"` +} + +type ArtifactKind string + +const ArtifactKindBundleArchive ArtifactKind = "bundle-archive" + +type ArtifactFormat string + +const ( + ArtifactFormatZip ArtifactFormat = "zip" + ArtifactFormatTarGz ArtifactFormat = "tar.gz" +) + +type ErrorInfo struct { + Code string `json:"code"` + Message string `json:"message"` +} + +type ResolveRequest struct { + App AppDescriptor +} + +type CheckRequest struct{} + +type DownloadedFile struct { + Path string + Size int64 + SHA256 string +} + +type InstallRoot struct { + Path string `json:"path"` +} + +type BundleManifest struct { + SchemaVersion int `json:"schemaVersion"` + EntryPoint string `json:"entrypoint"` + Files []BundleFile `json:"files"` +} + +type BundleFile struct { + Path string `json:"path"` + Mode string `json:"mode"` +} + +type StagedArtifact struct { + Path string `json:"path"` + Root InstallRoot `json:"root"` + Release Release `json:"release"` + Bundle BundleManifest `json:"bundle"` +} + +type ApplyRequest struct { + App AppDescriptor + Root InstallRoot + Staged StagedArtifact +} + +type ReleaseProvider interface { + Resolve(ctx context.Context, req ResolveRequest) (*Release, error) + OpenArtifact(ctx context.Context, rel Release) (io.ReadCloser, error) +} + +type SnapshotStore interface { + Load(ctx context.Context) (Snapshot, error) + Save(ctx context.Context, snapshot Snapshot) error + ClearStaged(ctx context.Context) error +} + +type Downloader interface { + Download(ctx context.Context, artifact Artifact) (DownloadedFile, error) +} + +type PlatformInstaller interface { + DetectInstallRoot(app AppDescriptor) (InstallRoot, error) + Stage(ctx context.Context, root InstallRoot, file DownloadedFile, release Release) (StagedArtifact, error) + SpawnApplyAndRestart(ctx context.Context, req ApplyRequest) error +} diff --git a/pkg/wails3kit/updates/wails/BUILD.bazel b/pkg/wails3kit/updates/wails/BUILD.bazel new file mode 100644 index 0000000..d29ded9 --- /dev/null +++ b/pkg/wails3kit/updates/wails/BUILD.bazel @@ -0,0 +1,23 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "wails", + srcs = ["service.go"], + importpath = "github.com/Eriyc/rules_wails/pkg/wails3kit/updates/wails", + visibility = ["//visibility:public"], + deps = [ + "//pkg/wails3kit/updates", + "@com_github_wailsapp_wails_v3//pkg/application", + ], +) + +go_test( + name = "wails_test", + srcs = ["service_test.go"], + embed = [":wails"], + deps = [ + "//pkg/wails3kit/updates", + "//pkg/wails3kit/updates/storage/file", + "@com_github_wailsapp_wails_v3//pkg/application", + ], +) diff --git a/pkg/wails3kit/updates/wails/service.go b/pkg/wails3kit/updates/wails/service.go new file mode 100644 index 0000000..de15d8f --- /dev/null +++ b/pkg/wails3kit/updates/wails/service.go @@ -0,0 +1,110 @@ +package wails + +import ( + "context" + + "github.com/Eriyc/rules_wails/pkg/wails3kit/updates" + "github.com/wailsapp/wails/v3/pkg/application" +) + +type Options struct { + App *application.App + Controller *updates.Controller + EventName string + AutoCheckOnStartup bool + Emitter func(string, any) +} + +type Service struct { + controller *updates.Controller + eventName string + autoCheckOnStartup bool + emitter func(string, any) + stop func() + done chan struct{} +} + +func RegisterEvents(eventName string) { + if eventName == "" { + eventName = "updates:state" + } + application.RegisterEvent[updates.Snapshot](eventName) +} + +func NewService(opts Options) *Service { + eventName := opts.EventName + if eventName == "" { + eventName = "updates:state" + } + + emitter := opts.Emitter + if emitter == nil && opts.App != nil { + emitter = func(name string, data any) { + opts.App.Event.Emit(name, data) + } + } + + return &Service{ + controller: opts.Controller, + eventName: eventName, + autoCheckOnStartup: opts.AutoCheckOnStartup, + emitter: emitter, + done: make(chan struct{}), + } +} + +func (service *Service) ServiceStartup(_ context.Context, _ application.ServiceOptions) error { + if service.controller == nil { + return updates.ErrInvalidConfig + } + + channel, stop := service.controller.Subscribe(4) + service.stop = stop + go func() { + for { + select { + case <-service.done: + return + case snapshot := <-channel: + if service.emitter != nil { + service.emitter(service.eventName, snapshot) + } + } + } + }() + + if service.autoCheckOnStartup { + go func() { + _, _ = service.Check() + }() + } + return nil +} + +func (service *Service) ServiceShutdown() error { + if service.stop != nil { + service.stop() + } + select { + case <-service.done: + default: + close(service.done) + } + return nil +} + +func (service *Service) Snapshot() updates.Snapshot { + return service.controller.Snapshot() +} + +func (service *Service) Check() (updates.Snapshot, error) { + return service.controller.Check(context.Background(), updates.CheckRequest{}) +} + +func (service *Service) Download() (updates.Snapshot, error) { + return service.controller.Download(context.Background()) +} + +func (service *Service) ApplyAndRestart() error { + return service.controller.ApplyAndRestart(context.Background()) +} diff --git a/pkg/wails3kit/updates/wails/service_test.go b/pkg/wails3kit/updates/wails/service_test.go new file mode 100644 index 0000000..2b97fa2 --- /dev/null +++ b/pkg/wails3kit/updates/wails/service_test.go @@ -0,0 +1,104 @@ +package wails + +import ( + "context" + "io" + "path/filepath" + "testing" + "time" + + "github.com/Eriyc/rules_wails/pkg/wails3kit/updates" + filestore "github.com/Eriyc/rules_wails/pkg/wails3kit/updates/storage/file" + "github.com/wailsapp/wails/v3/pkg/application" +) + +func TestServiceEmitsSnapshots(t *testing.T) { + t.Parallel() + + release := &updates.Release{ + Version: "1.1.0", + Channel: updates.ChannelStable, + Artifact: updates.Artifact{ + Kind: updates.ArtifactKindBundleArchive, + Format: updates.ArtifactFormatZip, + URL: "https://example.invalid/app.zip", + SHA256: "abc", + }, + } + controller, err := updates.NewController(updates.Config{ + App: updates.AppDescriptor{ + ProductID: "com.example.app", + CurrentVersion: "1.0.0", + Channel: updates.ChannelStable, + OS: "linux", + Arch: "amd64", + ExecutablePath: filepath.Join(t.TempDir(), "App"), + }, + Provider: serviceFakeProvider{release: release}, + Downloader: serviceFakeDownloader{}, + Store: filestore.New(filepath.Join(t.TempDir(), "snapshot.json")), + Platform: &serviceFakePlatform{root: updates.InstallRoot{Path: t.TempDir()}}, + }) + if err != nil { + t.Fatalf("NewController returned error: %v", err) + } + + emitted := make(chan updates.Snapshot, 2) + service := NewService(Options{ + Controller: controller, + Emitter: func(_ string, data any) { + emitted <- data.(updates.Snapshot) + }, + }) + if err := service.ServiceStartup(context.Background(), application.ServiceOptions{}); err != nil { + t.Fatalf("ServiceStartup returned error: %v", err) + } + defer service.ServiceShutdown() + + if _, err := service.Check(); err != nil { + t.Fatalf("Check returned error: %v", err) + } + + select { + case snapshot := <-emitted: + if snapshot.State == "" { + t.Fatal("expected emitted snapshot state") + } + case <-time.After(time.Second): + t.Fatal("timed out waiting for emitted snapshot") + } +} + +type serviceFakeProvider struct { + release *updates.Release +} + +func (provider serviceFakeProvider) Resolve(context.Context, updates.ResolveRequest) (*updates.Release, error) { + return provider.release, nil +} + +func (provider serviceFakeProvider) OpenArtifact(context.Context, updates.Release) (io.ReadCloser, error) { + return nil, nil +} + +type serviceFakeDownloader struct{} + +func (serviceFakeDownloader) Download(context.Context, updates.Artifact) (updates.DownloadedFile, error) { + return updates.DownloadedFile{}, nil +} + +type serviceFakePlatform struct { + root updates.InstallRoot +} + +func (platform *serviceFakePlatform) DetectInstallRoot(updates.AppDescriptor) (updates.InstallRoot, error) { + return platform.root, nil +} + +func (platform *serviceFakePlatform) Stage(context.Context, updates.InstallRoot, updates.DownloadedFile, updates.Release) (updates.StagedArtifact, error) { + return updates.StagedArtifact{}, nil +} + +func (platform *serviceFakePlatform) SpawnApplyAndRestart(context.Context, updates.ApplyRequest) error { + return nil +}