forked from joejulian/gio-cmd
all: initial import from gio main repository
Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
# SPDX-License-Identifier: Unlicense OR MIT
|
||||
image: debian/testing
|
||||
packages:
|
||||
- clang
|
||||
- cmake
|
||||
- curl
|
||||
- autoconf
|
||||
- libxml2-dev
|
||||
- libssl-dev
|
||||
- libz-dev
|
||||
- llvm-dev # for cctools
|
||||
- uuid-dev ## for cctools
|
||||
- libplist-utils # for gogio
|
||||
sources:
|
||||
- https://git.sr.ht/~eliasnaur/gio-cmd
|
||||
- https://git.sr.ht/~eliasnaur/applesdks
|
||||
- https://git.sr.ht/~eliasnaur/giouiorg
|
||||
- https://github.com/tpoechtrager/cctools-port.git
|
||||
- https://github.com/tpoechtrager/apple-libtapi.git
|
||||
- https://github.com/mackyle/xar.git
|
||||
environment:
|
||||
APPLE_TOOLCHAIN_ROOT: /home/build/appletools
|
||||
PATH: /home/build/sdk/go/bin:/home/build/go/bin:/usr/bin
|
||||
tasks:
|
||||
- install_go: |
|
||||
mkdir -p /home/build/sdk
|
||||
curl -s https://dl.google.com/go/go1.17.7.linux-amd64.tar.gz | tar -C /home/build/sdk -xzf -
|
||||
- prepare_toolchain: |
|
||||
mkdir -p $APPLE_TOOLCHAIN_ROOT
|
||||
cd $APPLE_TOOLCHAIN_ROOT
|
||||
tar xJf /home/build/applesdks/applesdks.tar.xz
|
||||
mkdir bin tools
|
||||
cd bin
|
||||
ln -s ../toolchain/bin/x86_64-apple-darwin19-ld ld
|
||||
ln -s ../toolchain/bin/x86_64-apple-darwin19-ar ar
|
||||
ln -s /home/build/cctools-port/cctools/misc/lipo lipo
|
||||
ln -s ../tools/appletoolchain xcrun
|
||||
ln -s /usr/bin/plistutil plutil
|
||||
cd ../tools
|
||||
ln -s appletoolchain clang-ios
|
||||
ln -s appletoolchain clang-macos
|
||||
- install_appletoolchain: |
|
||||
cd giouiorg
|
||||
go build -o $APPLE_TOOLCHAIN_ROOT/tools ./cmd/appletoolchain
|
||||
- build_xar: |
|
||||
cd xar/xar
|
||||
ac_cv_lib_crypto_OpenSSL_add_all_ciphers=yes CC=clang ./autogen.sh --prefix=/usr
|
||||
make
|
||||
sudo make install
|
||||
- build_libtapi: |
|
||||
cd apple-libtapi
|
||||
INSTALLPREFIX=$APPLE_TOOLCHAIN_ROOT/libtapi ./build.sh
|
||||
./install.sh
|
||||
- build_cctools: |
|
||||
cd cctools-port/cctools
|
||||
./configure --prefix $APPLE_TOOLCHAIN_ROOT/toolchain --with-libtapi=$APPLE_TOOLCHAIN_ROOT/libtapi --target=x86_64-apple-darwin19
|
||||
make install
|
||||
- install_gogio: |
|
||||
cd gio-cmd
|
||||
go install ./gogio
|
||||
- test_ios_gogio: |
|
||||
mkdir tmp
|
||||
cd tmp
|
||||
go mod init example.com
|
||||
go get -d gioui.org/example/kitchen
|
||||
export PATH=/home/build/appletools/bin:$PATH
|
||||
gogio -target ios -o app.app gioui.org/example/kitchen
|
||||
@@ -0,0 +1,22 @@
|
||||
# SPDX-License-Identifier: Unlicense OR MIT
|
||||
image: freebsd/13.x
|
||||
packages:
|
||||
- libX11
|
||||
- libxkbcommon
|
||||
- libXcursor
|
||||
- libXfixes
|
||||
- vulkan-headers
|
||||
- wayland
|
||||
- mesa-libs
|
||||
- xorg-vfbserver
|
||||
sources:
|
||||
- https://git.sr.ht/~eliasnaur/gio-cmd
|
||||
environment:
|
||||
PATH: /home/build/sdk/go/bin:/bin:/usr/local/bin:/usr/bin
|
||||
tasks:
|
||||
- install_go: |
|
||||
mkdir -p /home/build/sdk
|
||||
curl https://dl.google.com/go/go1.17.7.freebsd-amd64.tar.gz | tar -C /home/build/sdk -xzf -
|
||||
- test_cmd: |
|
||||
cd gio-cmd
|
||||
go test ./...
|
||||
@@ -0,0 +1,91 @@
|
||||
# SPDX-License-Identifier: Unlicense OR MIT
|
||||
image: debian/testing
|
||||
packages:
|
||||
- curl
|
||||
- pkg-config
|
||||
- libwayland-dev
|
||||
- libx11-dev
|
||||
- libx11-xcb-dev
|
||||
- libxkbcommon-dev
|
||||
- libxkbcommon-x11-dev
|
||||
- libgles2-mesa-dev
|
||||
- libegl1-mesa-dev
|
||||
- libffi-dev
|
||||
- libvulkan-dev
|
||||
- libxcursor-dev
|
||||
- libxrandr-dev
|
||||
- libxinerama-dev
|
||||
- libxi-dev
|
||||
- libxxf86vm-dev
|
||||
- mesa-vulkan-drivers
|
||||
- wine
|
||||
- xvfb
|
||||
- xdotool
|
||||
- scrot
|
||||
- sway
|
||||
- grim
|
||||
- wine
|
||||
- unzip
|
||||
sources:
|
||||
- https://git.sr.ht/~eliasnaur/gio-cmd
|
||||
environment:
|
||||
PATH: /home/build/sdk/go/bin:/usr/bin:/home/build/go/bin:/home/build/android/tools/bin
|
||||
ANDROID_SDK_ROOT: /home/build/android
|
||||
android_sdk_tools_zip: sdk-tools-linux-3859397.zip
|
||||
android_ndk_zip: android-ndk-r20-linux-x86_64.zip
|
||||
github_mirror: git@github.com:gioui/gio-cmd
|
||||
secrets:
|
||||
- fdc570bf-87f4-4528-8aee-4d1711b1c86f
|
||||
tasks:
|
||||
- install_go: |
|
||||
mkdir -p /home/build/sdk
|
||||
curl -s https://dl.google.com/go/go1.17.7.linux-amd64.tar.gz | tar -C /home/build/sdk -xzf -
|
||||
- check_gofmt: |
|
||||
cd gio-cmd
|
||||
test -z "$(gofmt -s -l .)"
|
||||
- check_sign_off: |
|
||||
set +x -e
|
||||
cd gio-cmd
|
||||
for hash in $(git log -n 20 --format="%H"); do
|
||||
message=$(git log -1 --format=%B $hash)
|
||||
if [[ ! "$message" =~ "Signed-off-by: " ]]; then
|
||||
echo "Missing 'Signed-off-by' in commit $hash"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
- mirror: |
|
||||
# mirror to github
|
||||
ssh-keyscan github.com > "$HOME"/.ssh/known_hosts && cd gio-cmd && git push --mirror "$github_mirror" || echo "failed mirroring"
|
||||
- install_chrome: |
|
||||
curl -s https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
|
||||
sudo sh -c 'echo "deb [arch=amd64] https://dl-ssl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'
|
||||
sudo apt-get -qq update
|
||||
sudo apt-get -qq install -y google-chrome-stable
|
||||
- test: |
|
||||
cd gio-cmd
|
||||
go test ./...
|
||||
go test -race ./...
|
||||
- install_jdk8: |
|
||||
curl -so jdk.deb "https://cdn.azul.com/zulu/bin/zulu8.42.0.21-ca-jdk8.0.232-linux_amd64.deb"
|
||||
sudo apt-get -qq install -y -f ./jdk.deb
|
||||
- install_android: |
|
||||
mkdir android
|
||||
cd android
|
||||
curl -so sdk-tools.zip https://dl.google.com/android/repository/$android_sdk_tools_zip
|
||||
unzip -q sdk-tools.zip
|
||||
rm sdk-tools.zip
|
||||
curl -so ndk.zip https://dl.google.com/android/repository/$android_ndk_zip
|
||||
unzip -q ndk.zip
|
||||
rm ndk.zip
|
||||
mv android-ndk-* ndk-bundle
|
||||
yes|sdkmanager --licenses
|
||||
sdkmanager "platforms;android-31" "build-tools;32.0.0"
|
||||
- install_gogio: |
|
||||
cd gio-cmd
|
||||
go install ./gogio
|
||||
- test_android_gogio: |
|
||||
mkdir tmp
|
||||
cd tmp
|
||||
go mod init example.com
|
||||
go get -d gioui.org/example/kitchen
|
||||
gogio -target android gioui.org/example/kitchen
|
||||
@@ -0,0 +1,18 @@
|
||||
# SPDX-License-Identifier: Unlicense OR MIT
|
||||
image: openbsd/latest
|
||||
packages:
|
||||
- libxkbcommon
|
||||
- go
|
||||
sources:
|
||||
- https://git.sr.ht/~eliasnaur/gio-cmd
|
||||
environment:
|
||||
PATH: /home/build/sdk/go/bin:/bin:/usr/local/bin:/usr/bin
|
||||
tasks:
|
||||
- install_go: |
|
||||
mkdir -p /home/build/sdk
|
||||
curl https://dl.google.com/go/go1.17.7.src.tar.gz | tar -C /home/build/sdk -xzf -
|
||||
cd /home/build/sdk/go/src
|
||||
./make.bash
|
||||
- test_cmd: |
|
||||
cd gio-cmd
|
||||
go test ./...
|
||||
@@ -0,0 +1,63 @@
|
||||
This project is provided under the terms of the UNLICENSE or
|
||||
the MIT license denoted by the following SPDX identifier:
|
||||
|
||||
SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
You may use the project under the terms of either license.
|
||||
|
||||
Both licenses are reproduced below.
|
||||
|
||||
----
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019 The Gio authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
---
|
||||
|
||||
|
||||
|
||||
---
|
||||
The UNLICENSE
|
||||
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <https://unlicense.org/>
|
||||
---
|
||||
@@ -0,0 +1,21 @@
|
||||
# Gio Tools
|
||||
|
||||
Tools for the [Gio project](https://gioui.org), most notably `gogio` for packaging Gio programs.
|
||||
|
||||
[](https://builds.sr.ht/~eliasnaur/gio-cmd)
|
||||
|
||||
## Issues
|
||||
|
||||
File bugs and TODOs through the [issue tracker](https://todo.sr.ht/~eliasnaur/gio) or send an email
|
||||
to [~eliasnaur/gio@todo.sr.ht](mailto:~eliasnaur/gio@todo.sr.ht). For general discussion, use the
|
||||
mailing list: [~eliasnaur/gio@lists.sr.ht](mailto:~eliasnaur/gio@lists.sr.ht).
|
||||
|
||||
## Contributing
|
||||
|
||||
Post discussion to the [mailing list](https://lists.sr.ht/~eliasnaur/gio) and patches to
|
||||
[gio-patches](https://lists.sr.ht/~eliasnaur/gio-patches). No Sourcehut
|
||||
account is required and you can post without being subscribed.
|
||||
|
||||
See the [contribution guide](https://gioui.org/doc/contribute) for more details.
|
||||
|
||||
An [official GitHub mirror](https://github.com/gioui/gio-cmd) is available.
|
||||
@@ -0,0 +1,31 @@
|
||||
module gioui.org/cmd
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
gioui.org v0.0.0-20220328154813-a3f147541fd0
|
||||
github.com/akavel/rsrc v0.10.1
|
||||
github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4
|
||||
github.com/chromedp/chromedp v0.5.2
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
|
||||
golang.org/x/text v0.3.6
|
||||
golang.org/x/tools v0.1.0
|
||||
)
|
||||
|
||||
require (
|
||||
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 // indirect
|
||||
gioui.org/shader v1.0.6 // indirect
|
||||
github.com/benoitkugler/textlayout v0.0.10 // indirect
|
||||
github.com/gioui/uax v0.2.1-0.20220325163150-e3d987515a12 // indirect
|
||||
github.com/go-text/typesetting v0.0.0-20220112121102-58fe93c84506 // indirect
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee // indirect
|
||||
github.com/gobwas/pool v0.2.0 // indirect
|
||||
github.com/gobwas/ws v1.0.2 // indirect
|
||||
github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08 // indirect
|
||||
github.com/mailru/easyjson v0.7.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20210722180016-6781d3edade3 // indirect
|
||||
golang.org/x/mod v0.4.2 // indirect
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
)
|
||||
@@ -0,0 +1,118 @@
|
||||
gioui.org v0.0.0-20220328154813-a3f147541fd0 h1:n4FUiCT6P4a2wF6hwX4a5R8TpjAhu/d+3nhwZW16MAI=
|
||||
gioui.org v0.0.0-20220328154813-a3f147541fd0/go.mod h1:b8vBukexG6eYuXZa14asjLAWJ+JjbZ/ophEnS2FjYUg=
|
||||
gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
|
||||
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc=
|
||||
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
|
||||
gioui.org/shader v1.0.6 h1:cvZmU+eODFR2545X+/8XucgZdTtEjR3QWW6W65b0q5Y=
|
||||
gioui.org/shader v1.0.6/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
|
||||
github.com/akavel/rsrc v0.10.1 h1:hCCPImjmFKVNGpeLZyTDRHEFC283DzyTXTo0cO0Rq9o=
|
||||
github.com/akavel/rsrc v0.10.1/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
|
||||
github.com/benoitkugler/pstokenizer v1.0.0/go.mod h1:l1G2Voirz0q/jj0TQfabNxVsa8HZXh/VMxFSRALWTiE=
|
||||
github.com/benoitkugler/textlayout v0.0.5/go.mod h1:puH4v13Uz7uIhIH0XMk5jgc8U3MXcn5r3VlV9K8n0D8=
|
||||
github.com/benoitkugler/textlayout v0.0.10 h1:uIaQgH4pBFw1LQ0tPkfjgxo94WYcckzzQaB41L2X84w=
|
||||
github.com/benoitkugler/textlayout v0.0.10/go.mod h1:puH4v13Uz7uIhIH0XMk5jgc8U3MXcn5r3VlV9K8n0D8=
|
||||
github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4 h1:QD3KxSJ59L2lxG6MXBjNHxiQO2RmxTQ3XcK+wO44WOg=
|
||||
github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4/go.mod h1:PfAWWKJqjlGFYJEidUM6aVIWPr0EpobeyVWEEmplX7g=
|
||||
github.com/chromedp/chromedp v0.5.2 h1:W8xBXQuUnd2dZK0SN/lyVwsQM7KgW+kY5HGnntms194=
|
||||
github.com/chromedp/chromedp v0.5.2/go.mod h1:rsTo/xRo23KZZwFmWk2Ui79rBaVRRATCjLzNQlOFSiA=
|
||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/gioui/uax v0.2.1-0.20220325163150-e3d987515a12 h1:1bjaB/5IIicfKpP4k0s30T2WEw//Kh00zULa8DQ0cxA=
|
||||
github.com/gioui/uax v0.2.1-0.20220325163150-e3d987515a12/go.mod h1:kDhBRTA/i3H46PVdhqcw26TdGSIj42TOKNWKY+Kipnw=
|
||||
github.com/go-text/typesetting v0.0.0-20220112121102-58fe93c84506 h1:1TPz/Gn/MsXwJ6bEtI9wdkPcQYr2X3V9I+wz4wPYUdY=
|
||||
github.com/go-text/typesetting v0.0.0-20220112121102-58fe93c84506/go.mod h1:R0mlTNeyszZ/tKQhbZA7SRGjx+OHsmNzgN2jTV7yZcs=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08 h1:V0an7KRw92wmJysvFvtqtKMAPmvS5O0jtB0nYo6t+gs=
|
||||
github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08/go.mod h1:dFWs1zEqDjFtnBXsd1vPOZaLsESovai349994nHx3e0=
|
||||
github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM=
|
||||
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20210722180016-6781d3edade3 h1:IlrJD2AM5p8JhN/wVny9jt6gJ9hut2VALhSeZ3SYluk=
|
||||
golang.org/x/exp v0.0.0-20210722180016-6781d3edade3/go.mod h1:DVyR6MI7P4kEQgvZJSj1fQGrWIi2RzIrfYWycwheUAc=
|
||||
golang.org/x/image v0.0.0-20210504121937-7319ad40d33e/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs=
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191113165036-4c7a9d0fe056/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
@@ -0,0 +1,143 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type AndroidTestDriver struct {
|
||||
driverBase
|
||||
|
||||
sdkDir string
|
||||
adbPath string
|
||||
}
|
||||
|
||||
var rxAdbDevice = regexp.MustCompile(`(.*)\s+device$`)
|
||||
|
||||
func (d *AndroidTestDriver) Start(path string) {
|
||||
d.sdkDir = os.Getenv("ANDROID_SDK_ROOT")
|
||||
if d.sdkDir == "" {
|
||||
d.Skipf("Android SDK is required; set $ANDROID_SDK_ROOT")
|
||||
}
|
||||
d.adbPath = filepath.Join(d.sdkDir, "platform-tools", "adb")
|
||||
if _, err := os.Stat(d.adbPath); os.IsNotExist(err) {
|
||||
d.Skipf("adb not found")
|
||||
}
|
||||
|
||||
devOut := bytes.TrimSpace(d.adb("devices"))
|
||||
devices := rxAdbDevice.FindAllSubmatch(devOut, -1)
|
||||
switch len(devices) {
|
||||
case 0:
|
||||
d.Skipf("no Android devices attached via adb; skipping")
|
||||
case 1:
|
||||
default:
|
||||
d.Skipf("multiple Android devices attached via adb; skipping")
|
||||
}
|
||||
|
||||
// If the device is attached but asleep, it's probably just charging.
|
||||
// Don't use it; the screen needs to be on and unlocked for the test to
|
||||
// work.
|
||||
if !bytes.Contains(
|
||||
d.adb("shell", "dumpsys", "power"),
|
||||
[]byte(" mWakefulness=Awake"),
|
||||
) {
|
||||
d.Skipf("Android device isn't awake; skipping")
|
||||
}
|
||||
|
||||
// First, build the app.
|
||||
apk := filepath.Join(d.tempDir("gio-endtoend-android"), "e2e.apk")
|
||||
d.gogio("-target=android", "-appid="+appid, "-o="+apk, path)
|
||||
|
||||
// Make sure the app isn't installed already, and try to uninstall it
|
||||
// when we finish. Previous failed test runs might have left the app.
|
||||
d.tryUninstall()
|
||||
d.adb("install", apk)
|
||||
d.Cleanup(d.tryUninstall)
|
||||
|
||||
// Force our e2e app to be fullscreen, so that the android system bar at
|
||||
// the top doesn't mess with our screenshots.
|
||||
// TODO(mvdan): is there a way to do this via gio, so that we don't need
|
||||
// to set up a global Android setting via the shell?
|
||||
d.adb("shell", "settings", "put", "global", "policy_control", "immersive.full="+appid)
|
||||
|
||||
// Make sure the app isn't already running.
|
||||
d.adb("shell", "pm", "clear", appid)
|
||||
|
||||
// Start listening for log messages.
|
||||
{
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cmd := exec.CommandContext(ctx, d.adbPath,
|
||||
"logcat",
|
||||
"-s", // suppress other logs
|
||||
"-T1", // don't show previous log messages
|
||||
appid+":*", // show all logs from our gio app ID
|
||||
)
|
||||
output, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
d.Fatal(err)
|
||||
}
|
||||
cmd.Stderr = cmd.Stdout
|
||||
d.output = output
|
||||
if err := cmd.Start(); err != nil {
|
||||
d.Fatal(err)
|
||||
}
|
||||
d.Cleanup(cancel)
|
||||
}
|
||||
|
||||
// Start the app.
|
||||
d.adb("shell", "monkey", "-p", appid, "1")
|
||||
|
||||
// Wait for the gio app to render.
|
||||
d.waitForFrame()
|
||||
}
|
||||
|
||||
func (d *AndroidTestDriver) Screenshot() image.Image {
|
||||
out := d.adb("shell", "screencap", "-p")
|
||||
img, err := png.Decode(bytes.NewReader(out))
|
||||
if err != nil {
|
||||
d.Fatal(err)
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
func (d *AndroidTestDriver) tryUninstall() {
|
||||
cmd := exec.Command(d.adbPath, "shell", "pm", "uninstall", appid)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
if bytes.Contains(out, []byte("Unknown package")) {
|
||||
// The package is not installed. Don't log anything.
|
||||
return
|
||||
}
|
||||
d.Logf("could not uninstall: %v\n%s", err, out)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *AndroidTestDriver) adb(args ...interface{}) []byte {
|
||||
strs := []string{}
|
||||
for _, arg := range args {
|
||||
strs = append(strs, fmt.Sprint(arg))
|
||||
}
|
||||
cmd := exec.Command(d.adbPath, strs...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
d.Errorf("%s", out)
|
||||
d.Fatal(err)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (d *AndroidTestDriver) Click(x, y int) {
|
||||
d.adb("shell", "input", "tap", x, y)
|
||||
|
||||
// Wait for the gio app to render after this click.
|
||||
d.waitForFrame()
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,156 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type buildInfo struct {
|
||||
appID string
|
||||
archs []string
|
||||
ldflags string
|
||||
minsdk int
|
||||
name string
|
||||
pkgDir string
|
||||
pkgPath string
|
||||
iconPath string
|
||||
tags string
|
||||
target string
|
||||
version int
|
||||
key string
|
||||
password string
|
||||
}
|
||||
|
||||
func newBuildInfo(pkgPath string) (*buildInfo, error) {
|
||||
pkgMetadata, err := getPkgMetadata(pkgPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
appID := getAppID(pkgMetadata)
|
||||
appIcon := filepath.Join(pkgMetadata.Dir, "appicon.png")
|
||||
if *iconPath != "" {
|
||||
appIcon = *iconPath
|
||||
}
|
||||
bi := &buildInfo{
|
||||
appID: appID,
|
||||
archs: getArchs(),
|
||||
ldflags: getLdFlags(appID),
|
||||
minsdk: *minsdk,
|
||||
name: getPkgName(pkgMetadata),
|
||||
pkgDir: pkgMetadata.Dir,
|
||||
pkgPath: pkgPath,
|
||||
iconPath: appIcon,
|
||||
tags: *extraTags,
|
||||
target: *target,
|
||||
version: *version,
|
||||
key: *signKey,
|
||||
password: *signPass,
|
||||
}
|
||||
return bi, nil
|
||||
}
|
||||
|
||||
func getArchs() []string {
|
||||
if *archNames != "" {
|
||||
return strings.Split(*archNames, ",")
|
||||
}
|
||||
switch *target {
|
||||
case "js":
|
||||
return []string{"wasm"}
|
||||
case "ios", "tvos":
|
||||
// Only 64-bit support.
|
||||
return []string{"arm64", "amd64"}
|
||||
case "android":
|
||||
return []string{"arm", "arm64", "386", "amd64"}
|
||||
case "windows":
|
||||
goarch := os.Getenv("GOARCH")
|
||||
if goarch == "" {
|
||||
goarch = runtime.GOARCH
|
||||
}
|
||||
return []string{goarch}
|
||||
default:
|
||||
// TODO: Add flag tests.
|
||||
panic("The target value has already been validated, this will never execute.")
|
||||
}
|
||||
}
|
||||
|
||||
func getLdFlags(appID string) string {
|
||||
var ldflags []string
|
||||
if extra := *extraLdflags; extra != "" {
|
||||
ldflags = append(ldflags, strings.Split(extra, " ")...)
|
||||
}
|
||||
// Pass appID along, to be used for logging on platforms like Android.
|
||||
ldflags = append(ldflags, fmt.Sprintf("-X gioui.org/app/internal/log.appID=%s", appID))
|
||||
// Pass along all remaining arguments to the app.
|
||||
if appArgs := flag.Args()[1:]; len(appArgs) > 0 {
|
||||
ldflags = append(ldflags, fmt.Sprintf("-X gioui.org/app.extraArgs=%s", strings.Join(appArgs, "|")))
|
||||
}
|
||||
if m := *linkMode; m != "" {
|
||||
ldflags = append(ldflags, "-linkmode="+m)
|
||||
}
|
||||
return strings.Join(ldflags, " ")
|
||||
}
|
||||
|
||||
type packageMetadata struct {
|
||||
PkgPath string
|
||||
Dir string
|
||||
}
|
||||
|
||||
func getPkgMetadata(pkgPath string) (*packageMetadata, error) {
|
||||
pkgImportPath, err := runCmd(exec.Command("go", "list", "-f", "{{.ImportPath}}", pkgPath))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pkgDir, err := runCmd(exec.Command("go", "list", "-f", "{{.Dir}}", pkgPath))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &packageMetadata{
|
||||
PkgPath: pkgImportPath,
|
||||
Dir: pkgDir,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getAppID(pkgMetadata *packageMetadata) string {
|
||||
if *appID != "" {
|
||||
return *appID
|
||||
}
|
||||
elems := strings.Split(pkgMetadata.PkgPath, "/")
|
||||
domain := strings.Split(elems[0], ".")
|
||||
name := ""
|
||||
if len(elems) > 1 {
|
||||
name = "." + elems[len(elems)-1]
|
||||
}
|
||||
if len(elems) < 2 && len(domain) < 2 {
|
||||
name = "." + domain[0]
|
||||
domain[0] = "localhost"
|
||||
} else {
|
||||
for i := 0; i < len(domain)/2; i++ {
|
||||
opp := len(domain) - 1 - i
|
||||
domain[i], domain[opp] = domain[opp], domain[i]
|
||||
}
|
||||
}
|
||||
|
||||
pkgDomain := strings.Join(domain, ".")
|
||||
appid := []rune(pkgDomain + name)
|
||||
|
||||
// a Java-language-style package name may contain upper- and lower-case
|
||||
// letters and underscores with individual parts separated by '.'.
|
||||
// https://developer.android.com/guide/topics/manifest/manifest-element
|
||||
for i, c := range appid {
|
||||
if !('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' ||
|
||||
c == '_' || c == '.') {
|
||||
appid[i] = '_'
|
||||
}
|
||||
}
|
||||
return string(appid)
|
||||
}
|
||||
|
||||
func getPkgName(pkgMetadata *packageMetadata) string {
|
||||
return path.Base(pkgMetadata.PkgPath)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
type expval struct {
|
||||
in, out string
|
||||
}
|
||||
|
||||
func TestAppID(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []expval{
|
||||
{"example", "localhost.example"},
|
||||
{"example.com", "com.example"},
|
||||
{"www.example.com", "com.example.www"},
|
||||
{"examplecom/app", "examplecom.app"},
|
||||
{"example.com/app", "com.example.app"},
|
||||
{"www.example.com/app", "com.example.www.app"},
|
||||
{"www.en.example.com/app", "com.example.en.www.app"},
|
||||
{"example.com/dir/app", "com.example.app"},
|
||||
{"example.com/dir.ext/app", "com.example.app"},
|
||||
{"example.com/dir/app.ext", "com.example.app.ext"},
|
||||
{"example-com.net/dir/app", "net.example_com.app"},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
got := getAppID(&packageMetadata{PkgPath: test.in})
|
||||
if exp := test.out; got != exp {
|
||||
t.Errorf("(%d): expected '%s', got '%s'", i, exp, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
/*
|
||||
The gogio tool builds and packages Gio programs for Android, iOS/tvOS
|
||||
and WebAssembly.
|
||||
|
||||
Run gogio with no arguments for instructions, or see the examples at
|
||||
https://gioui.org.
|
||||
*/
|
||||
package main
|
||||
@@ -0,0 +1,331 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var raceEnabled = false
|
||||
|
||||
var headless = flag.Bool("headless", true, "run end-to-end tests in headless mode")
|
||||
|
||||
const appid = "localhost.gogio.endtoend"
|
||||
|
||||
// TestDriver is implemented by each of the platforms we can run end-to-end
|
||||
// tests on. None of its methods return any errors, as the errors are directly
|
||||
// reported to testing.T via methods like Fatal.
|
||||
type TestDriver interface {
|
||||
initBase(t *testing.T, width, height int)
|
||||
|
||||
// Start opens the Gio app found at path. The driver should attempt to
|
||||
// run the app with the base driver's width and height, and the
|
||||
// platform's background should be white.
|
||||
//
|
||||
// When the function returns, the gio app must be ready to use on the
|
||||
// platform, with its initial frame fully drawn.
|
||||
Start(path string)
|
||||
|
||||
// Screenshot takes a screenshot of the Gio app on the platform.
|
||||
Screenshot() image.Image
|
||||
|
||||
// Click performs a pointer click at the specified coordinates,
|
||||
// including both press and release. It returns when the next frame is
|
||||
// fully drawn.
|
||||
Click(x, y int)
|
||||
}
|
||||
|
||||
type driverBase struct {
|
||||
*testing.T
|
||||
|
||||
width, height int
|
||||
|
||||
output io.Reader
|
||||
frameNotifs chan bool
|
||||
}
|
||||
|
||||
func (d *driverBase) initBase(t *testing.T, width, height int) {
|
||||
d.T = t
|
||||
d.width, d.height = width, height
|
||||
}
|
||||
|
||||
func TestEndToEnd(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skipf("end-to-end tests tend to be slow")
|
||||
}
|
||||
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
testdataWithGoImportPkgPath = "gioui.org/cmd/gogio/testdata"
|
||||
testdataWithRelativePkgPath = "testdata/testdata.go"
|
||||
)
|
||||
// Keep this list local, to not reuse TestDriver objects.
|
||||
subtests := []struct {
|
||||
name string
|
||||
driver TestDriver
|
||||
pkgPath string
|
||||
}{
|
||||
{"X11 using go import path", &X11TestDriver{}, testdataWithGoImportPkgPath},
|
||||
{"X11", &X11TestDriver{}, testdataWithRelativePkgPath},
|
||||
// Doesn't work on the builders.
|
||||
//{"Wayland", &WaylandTestDriver{}, testdataWithRelativePkgPath},
|
||||
{"JS", &JSTestDriver{}, testdataWithRelativePkgPath},
|
||||
{"Android", &AndroidTestDriver{}, testdataWithRelativePkgPath},
|
||||
{"Windows", &WineTestDriver{}, testdataWithRelativePkgPath},
|
||||
}
|
||||
|
||||
for _, subtest := range subtests {
|
||||
t.Run(subtest.name, func(t *testing.T) {
|
||||
subtest := subtest // copy the changing loop variable
|
||||
t.Parallel()
|
||||
runEndToEndTest(t, subtest.driver, subtest.pkgPath)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func runEndToEndTest(t *testing.T, driver TestDriver, pkgPath string) {
|
||||
size := image.Point{X: 800, Y: 600}
|
||||
driver.initBase(t, size.X, size.Y)
|
||||
|
||||
t.Log("starting driver and gio app")
|
||||
driver.Start(pkgPath)
|
||||
|
||||
beef := color.NRGBA{R: 0xde, G: 0xad, B: 0xbe, A: 0xff}
|
||||
white := color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}
|
||||
black := color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xff}
|
||||
gray := color.NRGBA{R: 0xbb, G: 0xbb, B: 0xbb, A: 0xff}
|
||||
red := color.NRGBA{R: 0xff, G: 0x00, B: 0x00, A: 0xff}
|
||||
|
||||
// These are the four colors at the beginning.
|
||||
t.Log("taking initial screenshot")
|
||||
withRetries(t, 4*time.Second, func() error {
|
||||
img := driver.Screenshot()
|
||||
size = img.Bounds().Size() // override the default size
|
||||
return checkImageCorners(img, beef, white, black, gray)
|
||||
})
|
||||
|
||||
// TODO(mvdan): implement this properly in the Wayland driver; swaymsg
|
||||
// almost works to automate clicks, but the button presses end up in the
|
||||
// wrong coordinates.
|
||||
if _, ok := driver.(*WaylandTestDriver); ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Click the first and last sections to turn them red.
|
||||
t.Log("clicking twice and taking another screenshot")
|
||||
driver.Click(1*(size.X/4), 1*(size.Y/4))
|
||||
driver.Click(3*(size.X/4), 3*(size.Y/4))
|
||||
withRetries(t, 4*time.Second, func() error {
|
||||
img := driver.Screenshot()
|
||||
return checkImageCorners(img, red, white, black, red)
|
||||
})
|
||||
}
|
||||
|
||||
// withRetries keeps retrying fn until it succeeds, or until the timeout is hit.
|
||||
// It uses a rudimentary kind of backoff, which starts with 100ms delays. As
|
||||
// such, timeout should generally be in the order of seconds.
|
||||
func withRetries(t *testing.T, timeout time.Duration, fn func() error) {
|
||||
t.Helper()
|
||||
|
||||
timeoutTimer := time.NewTimer(timeout)
|
||||
defer timeoutTimer.Stop()
|
||||
backoff := 100 * time.Millisecond
|
||||
|
||||
tries := 0
|
||||
var lastErr error
|
||||
for {
|
||||
if lastErr = fn(); lastErr == nil {
|
||||
return
|
||||
}
|
||||
tries++
|
||||
t.Logf("retrying after %s", backoff)
|
||||
|
||||
// Use a timer instead of a sleep, so that the timeout can stop
|
||||
// the backoff early. Don't reuse this timer, since we're not in
|
||||
// a hot loop, and we don't want tricky code.
|
||||
backoffTimer := time.NewTimer(backoff)
|
||||
defer backoffTimer.Stop()
|
||||
|
||||
select {
|
||||
case <-timeoutTimer.C:
|
||||
t.Errorf("last error: %v", lastErr)
|
||||
t.Fatalf("hit timeout of %s after %d tries", timeout, tries)
|
||||
case <-backoffTimer.C:
|
||||
}
|
||||
|
||||
// Keep doubling it until a maximum. With the start at 100ms,
|
||||
// we'll do: 100ms, 200ms, 400ms, 800ms, 1.6s, and 2s forever.
|
||||
backoff *= 2
|
||||
if max := 2 * time.Second; backoff > max {
|
||||
backoff = max
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type colorMismatch struct {
|
||||
x, y int
|
||||
wantRGB, gotRGB [3]uint32
|
||||
}
|
||||
|
||||
func (m colorMismatch) String() string {
|
||||
return fmt.Sprintf("%3d,%-3d got 0x%04x%04x%04x, want 0x%04x%04x%04x",
|
||||
m.x, m.y,
|
||||
m.gotRGB[0], m.gotRGB[1], m.gotRGB[2],
|
||||
m.wantRGB[0], m.wantRGB[1], m.wantRGB[2],
|
||||
)
|
||||
}
|
||||
|
||||
func checkImageCorners(img image.Image, topLeft, topRight, botLeft, botRight color.Color) error {
|
||||
// The colors are split in four rectangular sections. Check the corners
|
||||
// of each of the sections. We check the corners left to right, top to
|
||||
// bottom, like when reading left-to-right text.
|
||||
|
||||
size := img.Bounds().Size()
|
||||
var mismatches []colorMismatch
|
||||
|
||||
checkColor := func(x, y int, want color.Color) {
|
||||
r, g, b, _ := want.RGBA()
|
||||
got := img.At(x, y)
|
||||
r_, g_, b_, _ := got.RGBA()
|
||||
if r_ != r || g_ != g || b_ != b {
|
||||
mismatches = append(mismatches, colorMismatch{
|
||||
x: x,
|
||||
y: y,
|
||||
wantRGB: [3]uint32{r, g, b},
|
||||
gotRGB: [3]uint32{r_, g_, b_},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
minX, minY := 5, 5
|
||||
maxX, maxY := (size.X/2)-5, (size.Y/2)-5
|
||||
checkColor(minX, minY, topLeft)
|
||||
checkColor(maxX, minY, topLeft)
|
||||
checkColor(minX, maxY, topLeft)
|
||||
checkColor(maxX, maxY, topLeft)
|
||||
}
|
||||
{
|
||||
minX, minY := (size.X/2)+5, 5
|
||||
maxX, maxY := size.X-5, (size.Y/2)-5
|
||||
checkColor(minX, minY, topRight)
|
||||
checkColor(maxX, minY, topRight)
|
||||
checkColor(minX, maxY, topRight)
|
||||
checkColor(maxX, maxY, topRight)
|
||||
}
|
||||
{
|
||||
minX, minY := 5, (size.Y/2)+5
|
||||
maxX, maxY := (size.X/2)-5, size.Y-5
|
||||
checkColor(minX, minY, botLeft)
|
||||
checkColor(maxX, minY, botLeft)
|
||||
checkColor(minX, maxY, botLeft)
|
||||
checkColor(maxX, maxY, botLeft)
|
||||
}
|
||||
{
|
||||
minX, minY := (size.X/2)+5, (size.Y/2)+5
|
||||
maxX, maxY := size.X-5, size.Y-5
|
||||
checkColor(minX, minY, botRight)
|
||||
checkColor(maxX, minY, botRight)
|
||||
checkColor(minX, maxY, botRight)
|
||||
checkColor(maxX, maxY, botRight)
|
||||
}
|
||||
if n := len(mismatches); n > 0 {
|
||||
b := new(strings.Builder)
|
||||
fmt.Fprintf(b, "encountered %d color mismatches:\n", n)
|
||||
for _, m := range mismatches {
|
||||
fmt.Fprintf(b, "%s\n", m)
|
||||
}
|
||||
return errors.New(b.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *driverBase) waitForFrame() {
|
||||
d.Helper()
|
||||
|
||||
if d.frameNotifs == nil {
|
||||
// Start the goroutine that reads output lines and notifies of
|
||||
// new frames via frameNotifs. The test doesn't wait for this
|
||||
// goroutine to finish; it will naturally end when the output
|
||||
// reader reaches an error like EOF.
|
||||
d.frameNotifs = make(chan bool, 1)
|
||||
if d.output == nil {
|
||||
d.Fatal("need an output reader to be notified of frames")
|
||||
}
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(d.output)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
d.Log(line)
|
||||
if strings.Contains(line, "gio frame ready") {
|
||||
d.frameNotifs <- true
|
||||
}
|
||||
}
|
||||
// Since we're only interested in the output while the
|
||||
// app runs, and we don't know when it finishes here,
|
||||
// ignore "already closed" pipe errors.
|
||||
if err := scanner.Err(); err != nil && !errors.Is(err, os.ErrClosed) {
|
||||
d.Errorf("reading app output: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Unfortunately, there isn't a way to select on a test failing, since
|
||||
// testing.T doesn't have anything like a context or a "done" channel.
|
||||
//
|
||||
// We can't let selects block forever, since the default -test.timeout
|
||||
// is ten minutes - far too long for tests that take seconds.
|
||||
//
|
||||
// For now, a static short timeout is better than nothing. 5s is plenty
|
||||
// for our simple test app to render on any device.
|
||||
select {
|
||||
case <-d.frameNotifs:
|
||||
case <-time.After(5 * time.Second):
|
||||
d.Fatalf("timed out waiting for a frame to be ready")
|
||||
}
|
||||
}
|
||||
|
||||
func (d *driverBase) needPrograms(names ...string) {
|
||||
d.Helper()
|
||||
for _, name := range names {
|
||||
if _, err := exec.LookPath(name); err != nil {
|
||||
d.Skipf("%s needed to run", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *driverBase) tempDir(name string) string {
|
||||
d.Helper()
|
||||
dir, err := ioutil.TempDir("", name)
|
||||
if err != nil {
|
||||
d.Fatal(err)
|
||||
}
|
||||
d.Cleanup(func() { os.RemoveAll(dir) })
|
||||
return dir
|
||||
}
|
||||
|
||||
func (d *driverBase) gogio(args ...string) {
|
||||
d.Helper()
|
||||
prog, err := os.Executable()
|
||||
if err != nil {
|
||||
d.Fatal(err)
|
||||
}
|
||||
cmd := exec.Command(prog, args...)
|
||||
cmd.Env = append(os.Environ(), "RUN_GOGIO=1")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
d.Fatalf("gogio error: %s:\n%s", err, out)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package main
|
||||
|
||||
const mainUsage = `The gogio command builds and packages Gio (gioui.org) programs.
|
||||
|
||||
Usage:
|
||||
|
||||
gogio -target <target> [flags] <package> [run arguments]
|
||||
|
||||
The gogio tool builds and packages Gio programs for platforms where additional
|
||||
metadata or support files are required.
|
||||
|
||||
The package argument specifies an import path or a single Go source file to
|
||||
package. Any run arguments are appended to os.Args at runtime.
|
||||
|
||||
Compiled Java class files from jar files in the package directory are
|
||||
included in Android builds.
|
||||
|
||||
The mandatory -target flag selects the target platform: ios or android for the
|
||||
mobile platforms, tvos for Apple's tvOS, js for WebAssembly/WebGL.
|
||||
|
||||
The -arch flag specifies a comma separated list of GOARCHs to include. The
|
||||
default is all supported architectures.
|
||||
|
||||
The -o flag specifies an output file or directory, depending on the target.
|
||||
|
||||
The -buildmode flag selects the build mode. Two build modes are available, exe
|
||||
and archive. Buildmode exe outputs an .ipa file for iOS or tvOS, an .apk file
|
||||
for Android or a directory with the WebAssembly module and support files for
|
||||
a browser.
|
||||
|
||||
The -ldflags and -tags flags pass extra linker flags and tags to the go tool.
|
||||
|
||||
As a special case for iOS or tvOS, specifying a path that ends with ".app"
|
||||
will output an app directory suitable for a simulator.
|
||||
|
||||
The other buildmode is archive, which will output an .aar library for Android
|
||||
or a .framework for iOS and tvOS.
|
||||
|
||||
The -icon flag specifies a path to a PNG image to use as app icon on iOS and Android.
|
||||
If left unspecified, the appicon.png file from the main package is used
|
||||
(if it exists).
|
||||
|
||||
The -appid flag specifies the package name for Android or the bundle id for
|
||||
iOS and tvOS. A bundle id must be provisioned through Xcode before the gogio
|
||||
tool can use it.
|
||||
|
||||
The -version flag specifies the integer version code for Android and the last
|
||||
component of the 1.0.X version for iOS and tvOS.
|
||||
|
||||
For Android builds the -minsdk flag specify the minimum SDK level. For example,
|
||||
use -minsdk 22 to target Android 5.1 (Lollipop) and later.
|
||||
|
||||
For Windows builds the -minsdk flag specify the minimum OS version. For example,
|
||||
use -mindk 10 to target Windows 10 and later, -minsdk 6 for Windows Vista and later.
|
||||
|
||||
For iOS builds the -minsdk flag specify the minimum iOS version. For example,
|
||||
use -mindk 15 to target iOS 15.0 and later.
|
||||
|
||||
The -work flag prints the path to the working directory and suppress
|
||||
its deletion.
|
||||
|
||||
The -x flag will print all the external commands executed by the gogio tool.
|
||||
|
||||
The -signkey flag specifies the path of the keystore, used for signing Android apk/aab files.
|
||||
|
||||
The -signpass flag specifies the password of the keystore, ignored if -signkey is not provided.
|
||||
`
|
||||
@@ -0,0 +1,589 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
const (
|
||||
minIOSVersion = 10
|
||||
// Metal is available from iOS 8 on devices, yet from version 13 on the
|
||||
// simulator.
|
||||
minSimulatorVersion = 13
|
||||
)
|
||||
|
||||
func buildIOS(tmpDir, target string, bi *buildInfo) error {
|
||||
appName := bi.name
|
||||
switch *buildMode {
|
||||
case "archive":
|
||||
framework := *destPath
|
||||
if framework == "" {
|
||||
framework = fmt.Sprintf("%s.framework", strings.Title(appName))
|
||||
}
|
||||
return archiveIOS(tmpDir, target, framework, bi)
|
||||
case "exe":
|
||||
out := *destPath
|
||||
if out == "" {
|
||||
out = appName + ".ipa"
|
||||
}
|
||||
forDevice := strings.HasSuffix(out, ".ipa")
|
||||
// Filter out unsupported architectures.
|
||||
for i := len(bi.archs) - 1; i >= 0; i-- {
|
||||
switch bi.archs[i] {
|
||||
case "arm", "arm64":
|
||||
if forDevice {
|
||||
continue
|
||||
}
|
||||
case "386", "amd64":
|
||||
if !forDevice {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
bi.archs = append(bi.archs[:i], bi.archs[i+1:]...)
|
||||
}
|
||||
tmpFramework := filepath.Join(tmpDir, "Gio.framework")
|
||||
if err := archiveIOS(tmpDir, target, tmpFramework, bi); err != nil {
|
||||
return err
|
||||
}
|
||||
if !forDevice && !strings.HasSuffix(out, ".app") {
|
||||
return fmt.Errorf("the specified output directory %q does not end in .app or .ipa", out)
|
||||
}
|
||||
if !forDevice {
|
||||
return exeIOS(tmpDir, target, out, bi)
|
||||
}
|
||||
payload := filepath.Join(tmpDir, "Payload")
|
||||
appDir := filepath.Join(payload, appName+".app")
|
||||
if err := os.MkdirAll(appDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := exeIOS(tmpDir, target, appDir, bi); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := signIOS(bi, tmpDir, appDir); err != nil {
|
||||
return err
|
||||
}
|
||||
return zipDir(out, tmpDir, "Payload")
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
|
||||
func signIOS(bi *buildInfo, tmpDir, app string) error {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
provPattern := filepath.Join(home, "Library", "MobileDevice", "Provisioning Profiles", "*.mobileprovision")
|
||||
provisions, err := filepath.Glob(provPattern)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
provInfo := filepath.Join(tmpDir, "provision.plist")
|
||||
var avail []string
|
||||
for _, prov := range provisions {
|
||||
// Decode the provision file to a plist.
|
||||
_, err := runCmd(exec.Command("security", "cms", "-D", "-i", prov, "-o", provInfo))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
expUnix, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", "-c", "Print:ExpirationDate", provInfo))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
exp, err := time.Parse(time.UnixDate, expUnix)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sign: failed to parse expiration date from %q: %v", prov, err)
|
||||
}
|
||||
if exp.Before(time.Now()) {
|
||||
continue
|
||||
}
|
||||
appIDPrefix, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", "-c", "Print:ApplicationIdentifierPrefix:0", provInfo))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
provAppID, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", "-c", "Print:Entitlements:application-identifier", provInfo))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
expAppID := fmt.Sprintf("%s.%s", appIDPrefix, bi.appID)
|
||||
avail = append(avail, provAppID)
|
||||
if expAppID != provAppID {
|
||||
continue
|
||||
}
|
||||
// Copy provisioning file.
|
||||
embedded := filepath.Join(app, "embedded.mobileprovision")
|
||||
if err := copyFile(embedded, prov); err != nil {
|
||||
return err
|
||||
}
|
||||
certDER, err := runCmdRaw(exec.Command("/usr/libexec/PlistBuddy", "-c", "Print:DeveloperCertificates:0", provInfo))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Omit trailing newline.
|
||||
certDER = certDER[:len(certDER)-1]
|
||||
entitlements, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", "-x", "-c", "Print:Entitlements", provInfo))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entFile := filepath.Join(tmpDir, "entitlements.plist")
|
||||
if err := ioutil.WriteFile(entFile, []byte(entitlements), 0660); err != nil {
|
||||
return err
|
||||
}
|
||||
identity := sha1.Sum(certDER)
|
||||
idHex := hex.EncodeToString(identity[:])
|
||||
_, err = runCmd(exec.Command("codesign", "-s", idHex, "-v", "--entitlements", entFile, app))
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("sign: no valid provisioning profile found for bundle id %q among %v", bi.appID, avail)
|
||||
}
|
||||
|
||||
func exeIOS(tmpDir, target, app string, bi *buildInfo) error {
|
||||
if bi.appID == "" {
|
||||
return errors.New("app id is empty; use -appid to set it")
|
||||
}
|
||||
if err := os.RemoveAll(app); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Mkdir(app, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
mainm := filepath.Join(tmpDir, "main.m")
|
||||
const mainmSrc = `@import UIKit;
|
||||
@import Gio;
|
||||
|
||||
@interface GioAppDelegate : UIResponder <UIApplicationDelegate>
|
||||
@property (strong, nonatomic) UIWindow *window;
|
||||
@end
|
||||
|
||||
@implementation GioAppDelegate
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
|
||||
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
|
||||
GioViewController *controller = [[GioViewController alloc] initWithNibName:nil bundle:nil];
|
||||
self.window.rootViewController = controller;
|
||||
[self.window makeKeyAndVisible];
|
||||
return YES;
|
||||
}
|
||||
@end
|
||||
|
||||
int main(int argc, char * argv[]) {
|
||||
@autoreleasepool {
|
||||
return UIApplicationMain(argc, argv, nil, NSStringFromClass([GioAppDelegate class]));
|
||||
}
|
||||
}`
|
||||
if err := ioutil.WriteFile(mainm, []byte(mainmSrc), 0660); err != nil {
|
||||
return err
|
||||
}
|
||||
appName := strings.Title(bi.name)
|
||||
exe := filepath.Join(app, appName)
|
||||
lipo := exec.Command("xcrun", "lipo", "-o", exe, "-create")
|
||||
var builds errgroup.Group
|
||||
for _, a := range bi.archs {
|
||||
clang, cflags, err := iosCompilerFor(target, a, bi.minsdk)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
exeSlice := filepath.Join(tmpDir, "app-"+a)
|
||||
lipo.Args = append(lipo.Args, exeSlice)
|
||||
compile := exec.Command(clang, cflags...)
|
||||
compile.Args = append(compile.Args,
|
||||
"-Werror",
|
||||
"-fmodules",
|
||||
"-fobjc-arc",
|
||||
"-x", "objective-c",
|
||||
"-F", tmpDir,
|
||||
"-o", exeSlice,
|
||||
mainm,
|
||||
)
|
||||
builds.Go(func() error {
|
||||
_, err := runCmd(compile)
|
||||
return err
|
||||
})
|
||||
}
|
||||
if err := builds.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := runCmd(lipo); err != nil {
|
||||
return err
|
||||
}
|
||||
infoPlist := buildInfoPlist(bi)
|
||||
plistFile := filepath.Join(app, "Info.plist")
|
||||
if err := ioutil.WriteFile(plistFile, []byte(infoPlist), 0660); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := os.Stat(bi.iconPath); err == nil {
|
||||
assetPlist, err := iosIcons(bi, tmpDir, app, bi.iconPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Merge assets plist with Info.plist
|
||||
cmd := exec.Command(
|
||||
"/usr/libexec/PlistBuddy",
|
||||
"-c", "Merge "+assetPlist,
|
||||
plistFile,
|
||||
)
|
||||
if _, err := runCmd(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := runCmd(exec.Command("plutil", "-convert", "binary1", plistFile)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// iosIcons builds an asset catalog and compile it with the Xcode command actool.
|
||||
// iosIcons returns the asset plist file to be merged into Info.plist.
|
||||
func iosIcons(bi *buildInfo, tmpDir, appDir, icon string) (string, error) {
|
||||
assets := filepath.Join(tmpDir, "Assets.xcassets")
|
||||
if err := os.Mkdir(assets, 0700); err != nil {
|
||||
return "", err
|
||||
}
|
||||
appIcon := filepath.Join(assets, "AppIcon.appiconset")
|
||||
err := buildIcons(appIcon, icon, []iconVariant{
|
||||
{path: "ios_2x.png", size: 120},
|
||||
{path: "ios_3x.png", size: 180},
|
||||
// The App Store icon is not allowed to contain
|
||||
// transparent pixels.
|
||||
{path: "ios_store.png", size: 1024, fill: true},
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
contentJson := `{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "ios_2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "ios_3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "ios_store.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
]
|
||||
}`
|
||||
contentFile := filepath.Join(appIcon, "Contents.json")
|
||||
if err := ioutil.WriteFile(contentFile, []byte(contentJson), 0600); err != nil {
|
||||
return "", err
|
||||
}
|
||||
assetPlist := filepath.Join(tmpDir, "assets.plist")
|
||||
|
||||
minsdk := bi.minsdk
|
||||
if minsdk == 0 {
|
||||
minsdk = minIOSVersion
|
||||
}
|
||||
compile := exec.Command(
|
||||
"actool",
|
||||
"--compile", appDir,
|
||||
"--platform", iosPlatformFor(bi.target),
|
||||
"--minimum-deployment-target", strconv.Itoa(minsdk),
|
||||
"--app-icon", "AppIcon",
|
||||
"--output-partial-info-plist", assetPlist,
|
||||
assets)
|
||||
_, err = runCmd(compile)
|
||||
return assetPlist, err
|
||||
}
|
||||
|
||||
func buildInfoPlist(bi *buildInfo) string {
|
||||
appName := strings.Title(bi.name)
|
||||
platform := iosPlatformFor(bi.target)
|
||||
var supportPlatform string
|
||||
switch bi.target {
|
||||
case "ios":
|
||||
supportPlatform = "iPhoneOS"
|
||||
case "tvos":
|
||||
supportPlatform = "AppleTVOS"
|
||||
}
|
||||
return fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>%s</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>%s</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>%s</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.%d</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>%d</string>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array><string>arm64</string></array>
|
||||
<key>DTPlatformName</key>
|
||||
<string>%s</string>
|
||||
<key>DTPlatformVersion</key>
|
||||
<string>12.4</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>%d</string>
|
||||
<key>UIDeviceFamily</key>
|
||||
<array>
|
||||
<integer>1</integer>
|
||||
<integer>2</integer>
|
||||
</array>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>%s</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>DTCompiler</key>
|
||||
<string>com.apple.compilers.llvm.clang.1_0</string>
|
||||
<key>DTPlatformBuild</key>
|
||||
<string>16G73</string>
|
||||
<key>DTSDKBuild</key>
|
||||
<string>16G73</string>
|
||||
<key>DTSDKName</key>
|
||||
<string>%s12.4</string>
|
||||
<key>DTXcode</key>
|
||||
<string>1030</string>
|
||||
<key>DTXcodeBuild</key>
|
||||
<string>10G8</string>
|
||||
</dict>
|
||||
</plist>`, appName, bi.appID, appName, bi.version, bi.version, platform, minIOSVersion, supportPlatform, platform)
|
||||
}
|
||||
|
||||
func iosPlatformFor(target string) string {
|
||||
switch target {
|
||||
case "ios":
|
||||
return "iphoneos"
|
||||
case "tvos":
|
||||
return "appletvos"
|
||||
default:
|
||||
panic("invalid platform " + target)
|
||||
}
|
||||
}
|
||||
|
||||
func archiveIOS(tmpDir, target, frameworkRoot string, bi *buildInfo) error {
|
||||
framework := filepath.Base(frameworkRoot)
|
||||
const suf = ".framework"
|
||||
if !strings.HasSuffix(framework, suf) {
|
||||
return fmt.Errorf("the specified output %q does not end in '.framework'", frameworkRoot)
|
||||
}
|
||||
framework = framework[:len(framework)-len(suf)]
|
||||
if err := os.RemoveAll(frameworkRoot); err != nil {
|
||||
return err
|
||||
}
|
||||
frameworkDir := filepath.Join(frameworkRoot, "Versions", "A")
|
||||
for _, dir := range []string{"Headers", "Modules"} {
|
||||
p := filepath.Join(frameworkDir, dir)
|
||||
if err := os.MkdirAll(p, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
symlinks := [][2]string{
|
||||
{"Versions/Current/Headers", "Headers"},
|
||||
{"Versions/Current/Modules", "Modules"},
|
||||
{"Versions/Current/" + framework, framework},
|
||||
{"A", filepath.Join("Versions", "Current")},
|
||||
}
|
||||
for _, l := range symlinks {
|
||||
if err := os.Symlink(l[0], filepath.Join(frameworkRoot, l[1])); err != nil && !os.IsExist(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
exe := filepath.Join(frameworkDir, framework)
|
||||
lipo := exec.Command("xcrun", "lipo", "-o", exe, "-create")
|
||||
var builds errgroup.Group
|
||||
tags := bi.tags
|
||||
goos := "ios"
|
||||
supportsIOS, err := supportsGOOS("ios")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !supportsIOS {
|
||||
// Go 1.15 and earlier target iOS with GOOS=darwin, tags=ios.
|
||||
goos = "darwin"
|
||||
tags = "ios " + tags
|
||||
}
|
||||
for _, a := range bi.archs {
|
||||
clang, cflags, err := iosCompilerFor(target, a, bi.minsdk)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lib := filepath.Join(tmpDir, "gio-"+a)
|
||||
cmd := exec.Command(
|
||||
"go",
|
||||
"build",
|
||||
"-ldflags=-s -w "+bi.ldflags,
|
||||
"-buildmode=c-archive",
|
||||
"-o", lib,
|
||||
"-tags", tags,
|
||||
bi.pkgPath,
|
||||
)
|
||||
lipo.Args = append(lipo.Args, lib)
|
||||
cflagsLine := strings.Join(cflags, " ")
|
||||
cmd.Env = append(
|
||||
os.Environ(),
|
||||
"GOOS="+goos,
|
||||
"GOARCH="+a,
|
||||
"CGO_ENABLED=1",
|
||||
"CC="+clang,
|
||||
"CGO_CFLAGS="+cflagsLine,
|
||||
"CGO_LDFLAGS="+cflagsLine,
|
||||
)
|
||||
builds.Go(func() error {
|
||||
_, err := runCmd(cmd)
|
||||
return err
|
||||
})
|
||||
}
|
||||
if err := builds.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := runCmd(lipo); err != nil {
|
||||
return err
|
||||
}
|
||||
appDir, err := runCmd(exec.Command("go", "list", "-f", "{{.Dir}}", "gioui.org/app/"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
headerDst := filepath.Join(frameworkDir, "Headers", framework+".h")
|
||||
headerSrc := filepath.Join(appDir, "framework_ios.h")
|
||||
if err := copyFile(headerDst, headerSrc); err != nil {
|
||||
return err
|
||||
}
|
||||
module := fmt.Sprintf(`framework module "%s" {
|
||||
header "%[1]s.h"
|
||||
|
||||
export *
|
||||
}`, framework)
|
||||
moduleFile := filepath.Join(frameworkDir, "Modules", "module.modulemap")
|
||||
return ioutil.WriteFile(moduleFile, []byte(module), 0644)
|
||||
}
|
||||
|
||||
func supportsGOOS(wantGoos string) (bool, error) {
|
||||
geese, err := runCmd(exec.Command("go", "tool", "dist", "list"))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, pair := range strings.Split(geese, "\n") {
|
||||
s := strings.SplitN(pair, "/", 2)
|
||||
if len(s) != 2 {
|
||||
return false, fmt.Errorf("go tool dist list: invalid GOOS/GOARCH pair: %s", pair)
|
||||
}
|
||||
goos := s[0]
|
||||
if goos == wantGoos {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func iosCompilerFor(target, arch string, minsdk int) (string, []string, error) {
|
||||
var (
|
||||
platformSDK string
|
||||
platformOS string
|
||||
)
|
||||
switch target {
|
||||
case "ios":
|
||||
platformOS = "ios"
|
||||
platformSDK = "iphone"
|
||||
case "tvos":
|
||||
platformOS = "tvos"
|
||||
platformSDK = "appletv"
|
||||
}
|
||||
switch arch {
|
||||
case "arm", "arm64":
|
||||
platformSDK += "os"
|
||||
if minsdk == 0 {
|
||||
minsdk = minIOSVersion
|
||||
}
|
||||
case "386", "amd64":
|
||||
platformOS += "-simulator"
|
||||
platformSDK += "simulator"
|
||||
if minsdk == 0 {
|
||||
minsdk = minSimulatorVersion
|
||||
}
|
||||
default:
|
||||
return "", nil, fmt.Errorf("unsupported -arch: %s", arch)
|
||||
}
|
||||
sdkPath, err := runCmd(exec.Command("xcrun", "--sdk", platformSDK, "--show-sdk-path"))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
clang, err := runCmd(exec.Command("xcrun", "--sdk", platformSDK, "--find", "clang"))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
cflags := []string{
|
||||
"-fembed-bitcode",
|
||||
"-arch", allArchs[arch].iosArch,
|
||||
"-isysroot", sdkPath,
|
||||
"-m" + platformOS + "-version-min=" + strconv.Itoa(minsdk),
|
||||
}
|
||||
return clang, cflags, nil
|
||||
}
|
||||
|
||||
func zipDir(dst, base, dir string) (err error) {
|
||||
f, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if cerr := f.Close(); err == nil {
|
||||
err = cerr
|
||||
}
|
||||
}()
|
||||
zipf := zip.NewWriter(f)
|
||||
err = filepath.Walk(filepath.Join(base, dir), func(path string, f os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if f.IsDir() {
|
||||
return nil
|
||||
}
|
||||
rel := filepath.ToSlash(path[len(base)+1:])
|
||||
entry, err := zipf.Create(rel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
src, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer src.Close()
|
||||
_, err = io.Copy(entry, src)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return zipf.Close()
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"image"
|
||||
"image/png"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os/exec"
|
||||
|
||||
"github.com/chromedp/cdproto/runtime"
|
||||
"github.com/chromedp/chromedp"
|
||||
|
||||
_ "gioui.org/unit" // the build tool adds it to go.mod, so keep it there
|
||||
)
|
||||
|
||||
type JSTestDriver struct {
|
||||
driverBase
|
||||
|
||||
// ctx is the chromedp context.
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (d *JSTestDriver) Start(path string) {
|
||||
if raceEnabled {
|
||||
d.Skipf("js/wasm doesn't support -race; skipping")
|
||||
}
|
||||
|
||||
// First, build the app.
|
||||
dir := d.tempDir("gio-endtoend-js")
|
||||
d.gogio("-target=js", "-o="+dir, path)
|
||||
|
||||
// Second, start Chrome.
|
||||
opts := append(chromedp.DefaultExecAllocatorOptions[:],
|
||||
chromedp.Flag("headless", *headless),
|
||||
)
|
||||
|
||||
actx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
|
||||
d.Cleanup(cancel)
|
||||
|
||||
ctx, cancel := chromedp.NewContext(actx,
|
||||
// Send all logf/errf calls to t.Logf
|
||||
chromedp.WithLogf(d.Logf),
|
||||
)
|
||||
d.Cleanup(cancel)
|
||||
d.ctx = ctx
|
||||
|
||||
if err := chromedp.Run(ctx); err != nil {
|
||||
if errors.Is(err, exec.ErrNotFound) {
|
||||
d.Skipf("test requires Chrome to be installed: %v", err)
|
||||
return
|
||||
}
|
||||
d.Fatal(err)
|
||||
}
|
||||
pr, pw := io.Pipe()
|
||||
d.Cleanup(func() { pw.Close() })
|
||||
d.output = pr
|
||||
chromedp.ListenTarget(ctx, func(ev interface{}) {
|
||||
switch ev := ev.(type) {
|
||||
case *runtime.EventConsoleAPICalled:
|
||||
switch ev.Type {
|
||||
case "log", "info", "warning", "error":
|
||||
var b bytes.Buffer
|
||||
b.WriteString("console.")
|
||||
b.WriteString(string(ev.Type))
|
||||
b.WriteString("(")
|
||||
for i, arg := range ev.Args {
|
||||
if i > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
b.Write(arg.Value)
|
||||
}
|
||||
b.WriteString(")\n")
|
||||
pw.Write(b.Bytes())
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Third, serve the app folder, set the browser tab dimensions, and
|
||||
// navigate to the folder.
|
||||
ts := httptest.NewServer(http.FileServer(http.Dir(dir)))
|
||||
d.Cleanup(ts.Close)
|
||||
|
||||
if err := chromedp.Run(ctx,
|
||||
chromedp.EmulateViewport(int64(d.width), int64(d.height)),
|
||||
chromedp.Navigate(ts.URL),
|
||||
); err != nil {
|
||||
d.Fatal(err)
|
||||
}
|
||||
|
||||
// Wait for the gio app to render.
|
||||
d.waitForFrame()
|
||||
}
|
||||
|
||||
func (d *JSTestDriver) Screenshot() image.Image {
|
||||
var buf []byte
|
||||
if err := chromedp.Run(d.ctx,
|
||||
chromedp.CaptureScreenshot(&buf),
|
||||
); err != nil {
|
||||
d.Fatal(err)
|
||||
}
|
||||
img, err := png.Decode(bytes.NewReader(buf))
|
||||
if err != nil {
|
||||
d.Fatal(err)
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
func (d *JSTestDriver) Click(x, y int) {
|
||||
if err := chromedp.Run(d.ctx,
|
||||
chromedp.MouseClickXY(float64(x), float64(y)),
|
||||
); err != nil {
|
||||
d.Fatal(err)
|
||||
}
|
||||
|
||||
// Wait for the gio app to render after this click.
|
||||
d.waitForFrame()
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"golang.org/x/tools/go/packages"
|
||||
)
|
||||
|
||||
func buildJS(bi *buildInfo) error {
|
||||
out := *destPath
|
||||
if out == "" {
|
||||
out = bi.name
|
||||
}
|
||||
if err := os.MkdirAll(out, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.Command(
|
||||
"go",
|
||||
"build",
|
||||
"-ldflags="+bi.ldflags,
|
||||
"-tags="+bi.tags,
|
||||
"-o", filepath.Join(out, "main.wasm"),
|
||||
bi.pkgPath,
|
||||
)
|
||||
cmd.Env = append(
|
||||
os.Environ(),
|
||||
"GOOS=js",
|
||||
"GOARCH=wasm",
|
||||
)
|
||||
_, err := runCmd(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var faviconPath string
|
||||
if _, err := os.Stat(bi.iconPath); err == nil {
|
||||
// Copy icon to the output folder
|
||||
icon, err := ioutil.ReadFile(bi.iconPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ioutil.WriteFile(filepath.Join(out, filepath.Base(bi.iconPath)), icon, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
faviconPath = filepath.Base(bi.iconPath)
|
||||
}
|
||||
|
||||
indexTemplate, err := template.New("").Parse(jsIndex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
if err := indexTemplate.Execute(&b, struct {
|
||||
Name string
|
||||
Icon string
|
||||
}{
|
||||
Name: bi.name,
|
||||
Icon: faviconPath,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(filepath.Join(out, "index.html"), b.Bytes(), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
goroot, err := runCmd(exec.Command("go", "env", "GOROOT"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wasmJS := filepath.Join(goroot, "misc", "wasm", "wasm_exec.js")
|
||||
if _, err := os.Stat(wasmJS); err != nil {
|
||||
return fmt.Errorf("failed to find $GOROOT/misc/wasm/wasm_exec.js driver: %v", err)
|
||||
}
|
||||
pkgs, err := packages.Load(&packages.Config{
|
||||
Mode: packages.NeedName | packages.NeedFiles | packages.NeedImports | packages.NeedDeps,
|
||||
Env: append(os.Environ(), "GOOS=js", "GOARCH=wasm"),
|
||||
}, bi.pkgPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
extraJS, err := findPackagesJS(pkgs[0], make(map[string]bool))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return mergeJSFiles(filepath.Join(out, "wasm.js"), append([]string{wasmJS}, extraJS...)...)
|
||||
}
|
||||
|
||||
func findPackagesJS(p *packages.Package, visited map[string]bool) (extraJS []string, err error) {
|
||||
if len(p.GoFiles) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
js, err := filepath.Glob(filepath.Join(filepath.Dir(p.GoFiles[0]), "*_js.js"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
extraJS = append(extraJS, js...)
|
||||
for _, imp := range p.Imports {
|
||||
if !visited[imp.ID] {
|
||||
extra, err := findPackagesJS(imp, visited)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
extraJS = append(extraJS, extra...)
|
||||
visited[imp.ID] = true
|
||||
}
|
||||
}
|
||||
return extraJS, nil
|
||||
}
|
||||
|
||||
// mergeJSFiles will merge all files into a single `wasm.js`. It will prepend the jsSetGo
|
||||
// and append the jsStartGo.
|
||||
func mergeJSFiles(dst string, files ...string) (err error) {
|
||||
w, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if cerr := w.Close(); err != nil {
|
||||
err = cerr
|
||||
}
|
||||
}()
|
||||
_, err = io.Copy(w, strings.NewReader(jsSetGo))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range files {
|
||||
r, err := os.Open(files[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(w, r)
|
||||
r.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = io.Copy(w, strings.NewReader(jsStartGo))
|
||||
return err
|
||||
}
|
||||
|
||||
const (
|
||||
jsIndex = `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
{{ if .Icon }}<link rel="icon" href="{{.Icon}}" type="image/x-icon" />{{ end }}
|
||||
{{ if .Name }}<title>{{.Name}}</title>{{ end }}
|
||||
<script src="wasm.js"></script>
|
||||
<style>
|
||||
body,pre { margin:0;padding:0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>`
|
||||
// jsSetGo sets the `window.go` variable.
|
||||
jsSetGo = `(() => {
|
||||
window.go = {argv: [], env: {}, importObject: {go: {}}};
|
||||
const argv = new URLSearchParams(location.search).get("argv");
|
||||
if (argv) {
|
||||
window.go["argv"] = argv.split(" ");
|
||||
}
|
||||
})();`
|
||||
// jsStartGo initializes the main.wasm.
|
||||
jsStartGo = `(() => {
|
||||
defaultGo = new Go();
|
||||
Object.assign(defaultGo["argv"], defaultGo["argv"].concat(go["argv"]));
|
||||
Object.assign(defaultGo["env"], go["env"]);
|
||||
for (let key in go["importObject"]) {
|
||||
if (typeof defaultGo["importObject"][key] === "undefined") {
|
||||
defaultGo["importObject"][key] = {};
|
||||
}
|
||||
Object.assign(defaultGo["importObject"][key], go["importObject"][key]);
|
||||
}
|
||||
window.go = defaultGo;
|
||||
if (!WebAssembly.instantiateStreaming) { // polyfill
|
||||
WebAssembly.instantiateStreaming = async (resp, importObject) => {
|
||||
const source = await (await resp).arrayBuffer();
|
||||
return await WebAssembly.instantiate(source, importObject);
|
||||
};
|
||||
}
|
||||
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
|
||||
go.run(result.instance);
|
||||
});
|
||||
})();`
|
||||
)
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/image/draw"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
var (
|
||||
target = flag.String("target", "", "specify target (ios, tvos, android, js).\n")
|
||||
archNames = flag.String("arch", "", "specify architecture(s) to include (arm, arm64, amd64).")
|
||||
minsdk = flag.Int("minsdk", 0, "specify the minimum supported operating system level")
|
||||
buildMode = flag.String("buildmode", "exe", "specify buildmode (archive, exe)")
|
||||
destPath = flag.String("o", "", "output file or directory.\nFor -target ios or tvos, use the .app suffix to target simulators.")
|
||||
appID = flag.String("appid", "", "app identifier (for -buildmode=exe)")
|
||||
version = flag.Int("version", 1, "app version (for -buildmode=exe)")
|
||||
printCommands = flag.Bool("x", false, "print the commands")
|
||||
keepWorkdir = flag.Bool("work", false, "print the name of the temporary work directory and do not delete it when exiting.")
|
||||
linkMode = flag.String("linkmode", "", "set the -linkmode flag of the go tool")
|
||||
extraLdflags = flag.String("ldflags", "", "extra flags to the Go linker")
|
||||
extraTags = flag.String("tags", "", "extra tags to the Go tool")
|
||||
iconPath = flag.String("icon", "", "specify an icon for iOS and Android")
|
||||
signKey = flag.String("signkey", "", "specify the path of the keystore to be used to sign Android apk files.")
|
||||
signPass = flag.String("signpass", "", "specify the password to decrypt the signkey.")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Usage = func() {
|
||||
fmt.Fprint(os.Stderr, mainUsage)
|
||||
}
|
||||
flag.Parse()
|
||||
if err := flagValidate(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gogio: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
buildInfo, err := newBuildInfo(flag.Arg(0))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gogio: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := build(buildInfo); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gogio: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func flagValidate() error {
|
||||
pkgPathArg := flag.Arg(0)
|
||||
if pkgPathArg == "" {
|
||||
return errors.New("specify a package")
|
||||
}
|
||||
if *target == "" {
|
||||
return errors.New("please specify -target")
|
||||
}
|
||||
switch *target {
|
||||
case "ios", "tvos", "android", "js", "windows":
|
||||
default:
|
||||
return fmt.Errorf("invalid -target %s", *target)
|
||||
}
|
||||
switch *buildMode {
|
||||
case "archive", "exe":
|
||||
default:
|
||||
return fmt.Errorf("invalid -buildmode %s", *buildMode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func build(bi *buildInfo) error {
|
||||
tmpDir, err := ioutil.TempDir("", "gogio-")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *keepWorkdir {
|
||||
fmt.Fprintf(os.Stderr, "WORKDIR=%s\n", tmpDir)
|
||||
} else {
|
||||
defer os.RemoveAll(tmpDir)
|
||||
}
|
||||
switch *target {
|
||||
case "js":
|
||||
return buildJS(bi)
|
||||
case "ios", "tvos":
|
||||
return buildIOS(tmpDir, *target, bi)
|
||||
case "android":
|
||||
return buildAndroid(tmpDir, bi)
|
||||
case "windows":
|
||||
return buildWindows(tmpDir, bi)
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
|
||||
func runCmdRaw(cmd *exec.Cmd) ([]byte, error) {
|
||||
if *printCommands {
|
||||
fmt.Printf("%s\n", strings.Join(cmd.Args, " "))
|
||||
}
|
||||
out, err := cmd.Output()
|
||||
if err == nil {
|
||||
return out, nil
|
||||
}
|
||||
if err, ok := err.(*exec.ExitError); ok {
|
||||
return nil, fmt.Errorf("%s failed: %s%s", strings.Join(cmd.Args, " "), out, err.Stderr)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func runCmd(cmd *exec.Cmd) (string, error) {
|
||||
out, err := runCmdRaw(cmd)
|
||||
return string(bytes.TrimSpace(out)), err
|
||||
}
|
||||
|
||||
func copyFile(dst, src string) (err error) {
|
||||
r, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
w, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if cerr := w.Close(); err == nil {
|
||||
err = cerr
|
||||
}
|
||||
}()
|
||||
_, err = io.Copy(w, r)
|
||||
return err
|
||||
}
|
||||
|
||||
type arch struct {
|
||||
iosArch string
|
||||
jniArch string
|
||||
clangArch string
|
||||
}
|
||||
|
||||
var allArchs = map[string]arch{
|
||||
"arm": {
|
||||
iosArch: "armv7",
|
||||
jniArch: "armeabi-v7a",
|
||||
clangArch: "armv7a-linux-androideabi",
|
||||
},
|
||||
"arm64": {
|
||||
iosArch: "arm64",
|
||||
jniArch: "arm64-v8a",
|
||||
clangArch: "aarch64-linux-android",
|
||||
},
|
||||
"386": {
|
||||
iosArch: "i386",
|
||||
jniArch: "x86",
|
||||
clangArch: "i686-linux-android",
|
||||
},
|
||||
"amd64": {
|
||||
iosArch: "x86_64",
|
||||
jniArch: "x86_64",
|
||||
clangArch: "x86_64-linux-android",
|
||||
},
|
||||
}
|
||||
|
||||
type iconVariant struct {
|
||||
path string
|
||||
size int
|
||||
fill bool
|
||||
}
|
||||
|
||||
func buildIcons(baseDir, icon string, variants []iconVariant) error {
|
||||
f, err := os.Open(icon)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
img, _, err := image.Decode(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var resizes errgroup.Group
|
||||
for _, v := range variants {
|
||||
v := v
|
||||
resizes.Go(func() (err error) {
|
||||
path := filepath.Join(baseDir, v.path)
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if cerr := f.Close(); err == nil {
|
||||
err = cerr
|
||||
}
|
||||
}()
|
||||
return png.Encode(f, resizeIcon(v, img))
|
||||
})
|
||||
}
|
||||
return resizes.Wait()
|
||||
}
|
||||
|
||||
func resizeIcon(v iconVariant, img image.Image) *image.NRGBA {
|
||||
scaled := image.NewNRGBA(image.Rectangle{Max: image.Point{X: v.size, Y: v.size}})
|
||||
op := draw.Src
|
||||
if v.fill {
|
||||
op = draw.Over
|
||||
draw.Draw(scaled, scaled.Bounds(), &image.Uniform{color.White}, image.Point{}, draw.Src)
|
||||
}
|
||||
draw.CatmullRom.Scale(scaled, scaled.Bounds(), img, img.Bounds(), op, nil)
|
||||
|
||||
return scaled
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if os.Getenv("RUN_GOGIO") != "" {
|
||||
// Allow the end-to-end tests to call the gogio tool without
|
||||
// having to build it from scratch, nor having to refactor the
|
||||
// main function to avoid using global variables.
|
||||
main()
|
||||
os.Exit(0) // main already exits, but just in case.
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package main
|
||||
|
||||
var AndroidPermissions = map[string][]string{
|
||||
"network": {
|
||||
"android.permission.INTERNET",
|
||||
},
|
||||
"networkstate": {
|
||||
"android.permission.ACCESS_NETWORK_STATE",
|
||||
},
|
||||
"bluetooth": {
|
||||
"android.permission.BLUETOOTH",
|
||||
"android.permission.BLUETOOTH_ADMIN",
|
||||
"android.permission.ACCESS_FINE_LOCATION",
|
||||
},
|
||||
"camera": {
|
||||
"android.permission.CAMERA",
|
||||
},
|
||||
"storage": {
|
||||
"android.permission.READ_EXTERNAL_STORAGE",
|
||||
"android.permission.WRITE_EXTERNAL_STORAGE",
|
||||
},
|
||||
}
|
||||
|
||||
var AndroidFeatures = map[string][]string{
|
||||
"default": {`glEsVersion="0x00020000"`, `name="android.hardware.type.pc"`},
|
||||
"bluetooth": {
|
||||
`name="android.hardware.bluetooth"`,
|
||||
`name="android.hardware.bluetooth_le"`,
|
||||
},
|
||||
"camera": {
|
||||
`name="android.hardware.camera"`,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
//go:build race
|
||||
// +build race
|
||||
|
||||
package main_test
|
||||
|
||||
func init() { raceEnabled = true }
|
||||
Vendored
+142
@@ -0,0 +1,142 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
// A simple app used for gogio's end-to-end tests.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"log"
|
||||
|
||||
"gioui.org/app"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
)
|
||||
|
||||
func main() {
|
||||
go func() {
|
||||
w := app.NewWindow()
|
||||
if err := loop(w); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
app.Main()
|
||||
}
|
||||
|
||||
type notifyFrame int
|
||||
|
||||
const (
|
||||
notifyNone notifyFrame = iota
|
||||
notifyInvalidate
|
||||
notifyPrint
|
||||
)
|
||||
|
||||
// notify keeps track of whether we want to print to stdout to notify the user
|
||||
// when a frame is ready. Initially we want to notify about the first frame.
|
||||
var notify = notifyInvalidate
|
||||
|
||||
type (
|
||||
C = layout.Context
|
||||
D = layout.Dimensions
|
||||
)
|
||||
|
||||
func loop(w *app.Window) error {
|
||||
topLeft := quarterWidget{
|
||||
color: color.NRGBA{R: 0xde, G: 0xad, B: 0xbe, A: 0xff},
|
||||
}
|
||||
topRight := quarterWidget{
|
||||
color: color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff},
|
||||
}
|
||||
botLeft := quarterWidget{
|
||||
color: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xff},
|
||||
}
|
||||
botRight := quarterWidget{
|
||||
color: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x80},
|
||||
}
|
||||
|
||||
var ops op.Ops
|
||||
for {
|
||||
e := <-w.Events()
|
||||
switch e := e.(type) {
|
||||
case system.DestroyEvent:
|
||||
return e.Err
|
||||
case system.FrameEvent:
|
||||
gtx := layout.NewContext(&ops, e)
|
||||
// Clear background to white, even on embedded platforms such as webassembly.
|
||||
paint.Fill(gtx.Ops, color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff})
|
||||
layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
// r1c1
|
||||
layout.Flexed(1, func(gtx C) D { return topLeft.Layout(gtx) }),
|
||||
// r1c2
|
||||
layout.Flexed(1, func(gtx C) D { return topRight.Layout(gtx) }),
|
||||
)
|
||||
}),
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
// r2c1
|
||||
layout.Flexed(1, func(gtx C) D { return botLeft.Layout(gtx) }),
|
||||
// r2c2
|
||||
layout.Flexed(1, func(gtx C) D { return botRight.Layout(gtx) }),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
e.Frame(gtx.Ops)
|
||||
|
||||
switch notify {
|
||||
case notifyInvalidate:
|
||||
notify = notifyPrint
|
||||
w.Invalidate()
|
||||
case notifyPrint:
|
||||
notify = notifyNone
|
||||
fmt.Println("gio frame ready")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// quarterWidget paints a quarter of the screen with one color. When clicked, it
|
||||
// turns red, going back to its normal color when clicked again.
|
||||
type quarterWidget struct {
|
||||
color color.NRGBA
|
||||
|
||||
clicked bool
|
||||
}
|
||||
|
||||
var red = color.NRGBA{R: 0xff, G: 0x00, B: 0x00, A: 0xff}
|
||||
|
||||
func (w *quarterWidget) Layout(gtx layout.Context) layout.Dimensions {
|
||||
var color color.NRGBA
|
||||
if w.clicked {
|
||||
color = red
|
||||
} else {
|
||||
color = w.color
|
||||
}
|
||||
|
||||
r := image.Rectangle{Max: gtx.Constraints.Max}
|
||||
paint.FillShape(gtx.Ops, color, clip.Rect(r).Op())
|
||||
|
||||
defer clip.Rect(image.Rectangle{
|
||||
Max: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y),
|
||||
}).Push(gtx.Ops).Pop()
|
||||
pointer.InputOp{
|
||||
Tag: w,
|
||||
Types: pointer.Press,
|
||||
}.Add(gtx.Ops)
|
||||
|
||||
for _, e := range gtx.Events(w) {
|
||||
if e, ok := e.(pointer.Event); ok && e.Type == pointer.Press {
|
||||
w.clicked = !w.clicked
|
||||
// notify when we're done updating the frame.
|
||||
notify = notifyInvalidate
|
||||
}
|
||||
}
|
||||
return layout.Dimensions{Size: gtx.Constraints.Max}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
type WaylandTestDriver struct {
|
||||
driverBase
|
||||
|
||||
runtimeDir string
|
||||
socket string
|
||||
display string
|
||||
}
|
||||
|
||||
// No bars or anything fancy. Just a white background with our dimensions.
|
||||
var tmplSwayConfig = template.Must(template.New("").Parse(`
|
||||
output * bg #FFFFFF solid_color
|
||||
output * mode {{.Width}}x{{.Height}}
|
||||
default_border none
|
||||
`))
|
||||
|
||||
var rxSwayReady = regexp.MustCompile(`Running compositor on wayland display '(.*)'`)
|
||||
|
||||
func (d *WaylandTestDriver) Start(path string) {
|
||||
// We want os.Environ, so that it can e.g. find $DISPLAY to run within
|
||||
// X11. wlroots env vars are documented at:
|
||||
// https://github.com/swaywm/wlroots/blob/master/docs/env_vars.md
|
||||
env := os.Environ()
|
||||
if *headless {
|
||||
env = append(env, "WLR_BACKENDS=headless")
|
||||
}
|
||||
|
||||
d.needPrograms(
|
||||
"sway", // to run a wayland compositor
|
||||
"grim", // to take screenshots
|
||||
"swaymsg", // to send input
|
||||
)
|
||||
|
||||
// First, build the app.
|
||||
dir := d.tempDir("gio-endtoend-wayland")
|
||||
bin := filepath.Join(dir, "red")
|
||||
flags := []string{"build", "-tags", "nox11", "-o=" + bin}
|
||||
if raceEnabled {
|
||||
flags = append(flags, "-race")
|
||||
}
|
||||
flags = append(flags, path)
|
||||
cmd := exec.Command("go", flags...)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
d.Fatalf("could not build app: %s:\n%s", err, out)
|
||||
}
|
||||
|
||||
conf := filepath.Join(dir, "config")
|
||||
f, err := os.Create(conf)
|
||||
if err != nil {
|
||||
d.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
if err := tmplSwayConfig.Execute(f, struct{ Width, Height int }{
|
||||
d.width, d.height,
|
||||
}); err != nil {
|
||||
d.Fatal(err)
|
||||
}
|
||||
|
||||
d.socket = filepath.Join(dir, "socket")
|
||||
env = append(env, "SWAYSOCK="+d.socket)
|
||||
d.runtimeDir = dir
|
||||
env = append(env, "XDG_RUNTIME_DIR="+d.runtimeDir)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
d.Cleanup(wg.Wait)
|
||||
|
||||
// First, start sway.
|
||||
{
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cmd := exec.CommandContext(ctx, "sway", "--config", conf, "--verbose")
|
||||
cmd.Env = env
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
d.Fatal(err)
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
d.Fatal(err)
|
||||
}
|
||||
d.Cleanup(cancel)
|
||||
d.Cleanup(func() {
|
||||
// Give it a chance to exit gracefully, cleaning up
|
||||
// after itself. After 10ms, the deferred cancel above
|
||||
// will signal an os.Kill.
|
||||
cmd.Process.Signal(os.Interrupt)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
})
|
||||
|
||||
// Wait for sway to be ready. We probably don't need a deadline
|
||||
// here.
|
||||
br := bufio.NewReader(stderr)
|
||||
for {
|
||||
line, err := br.ReadString('\n')
|
||||
if err != nil {
|
||||
d.Fatal(err)
|
||||
}
|
||||
if m := rxSwayReady.FindStringSubmatch(line); m != nil {
|
||||
d.display = m[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
if err := cmd.Wait(); err != nil && ctx.Err() == nil && !strings.Contains(err.Error(), "interrupt") {
|
||||
// Don't print all stderr, since we use --verbose.
|
||||
// TODO(mvdan): if it's useful, probably filter
|
||||
// errors and show them.
|
||||
d.Error(err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
// Then, start our program on the sway compositor above.
|
||||
{
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cmd := exec.CommandContext(ctx, bin)
|
||||
cmd.Env = []string{"XDG_RUNTIME_DIR=" + d.runtimeDir, "WAYLAND_DISPLAY=" + d.display}
|
||||
output, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
d.Fatal(err)
|
||||
}
|
||||
cmd.Stderr = cmd.Stdout
|
||||
d.output = output
|
||||
if err := cmd.Start(); err != nil {
|
||||
d.Fatal(err)
|
||||
}
|
||||
d.Cleanup(cancel)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
if err := cmd.Wait(); err != nil && ctx.Err() == nil {
|
||||
d.Error(err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait for the gio app to render.
|
||||
d.waitForFrame()
|
||||
}
|
||||
|
||||
func (d *WaylandTestDriver) Screenshot() image.Image {
|
||||
cmd := exec.Command("grim", "/dev/stdout")
|
||||
cmd.Env = []string{"XDG_RUNTIME_DIR=" + d.runtimeDir, "WAYLAND_DISPLAY=" + d.display}
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
d.Errorf("%s", out)
|
||||
d.Fatal(err)
|
||||
}
|
||||
img, err := png.Decode(bytes.NewReader(out))
|
||||
if err != nil {
|
||||
d.Fatal(err)
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
func (d *WaylandTestDriver) swaymsg(args ...interface{}) {
|
||||
strs := []string{"--socket", d.socket}
|
||||
for _, arg := range args {
|
||||
strs = append(strs, fmt.Sprint(arg))
|
||||
}
|
||||
cmd := exec.Command("swaymsg", strs...)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
d.Errorf("%s", out)
|
||||
d.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *WaylandTestDriver) Click(x, y int) {
|
||||
d.swaymsg("seat", "-", "cursor", "set", x, y)
|
||||
d.swaymsg("seat", "-", "cursor", "press", "button1")
|
||||
d.swaymsg("seat", "-", "cursor", "release", "button1")
|
||||
|
||||
// Wait for the gio app to render after this click.
|
||||
d.waitForFrame()
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"image"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/image/draw"
|
||||
)
|
||||
|
||||
// Wine is tightly coupled with X11 at the moment, and we can reuse the same
|
||||
// methods to automate screenshots and clicks. The main difference is how we
|
||||
// build and run the app.
|
||||
|
||||
// The only quirk is that it seems impossible for the Wine window to take the
|
||||
// entirety of the X server's dimensions, even if we try to resize it to take
|
||||
// the entire display. It seems to want to leave some vertical space empty,
|
||||
// presumably for window decorations or the "start" bar on Windows. To work
|
||||
// around that, make the X server 50x50px bigger, and crop the screenshots back
|
||||
// to the original size.
|
||||
|
||||
type WineTestDriver struct {
|
||||
X11TestDriver
|
||||
}
|
||||
|
||||
func (d *WineTestDriver) Start(path string) {
|
||||
d.needPrograms("wine")
|
||||
|
||||
// First, build the app.
|
||||
bin := filepath.Join(d.tempDir("gio-endtoend-windows"), "red.exe")
|
||||
flags := []string{"build", "-o=" + bin}
|
||||
if raceEnabled {
|
||||
if runtime.GOOS != "windows" {
|
||||
// cross-compilation disables CGo, which breaks -race.
|
||||
d.Skipf("can't cross-compile -race for Windows; skipping")
|
||||
}
|
||||
flags = append(flags, "-race")
|
||||
}
|
||||
flags = append(flags, path)
|
||||
cmd := exec.Command("go", flags...)
|
||||
cmd.Env = os.Environ()
|
||||
cmd.Env = append(cmd.Env, "GOOS=windows")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
d.Fatalf("could not build app: %s:\n%s", err, out)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
d.Cleanup(wg.Wait)
|
||||
|
||||
// Add 50x50px to the display dimensions, as discussed earlier.
|
||||
d.startServer(&wg, d.width+50, d.height+50)
|
||||
|
||||
// Then, start our program via Wine on the X server above.
|
||||
{
|
||||
cacheDir, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
d.Fatal(err)
|
||||
}
|
||||
// Use a wine directory separate from the default ~/.wine, so
|
||||
// that the user's winecfg doesn't affect our test. This will
|
||||
// default to ~/.cache/gio-e2e-wine. We use the user's cache,
|
||||
// to reuse a previously set up wineprefix.
|
||||
wineprefix := filepath.Join(cacheDir, "gio-e2e-wine")
|
||||
|
||||
// First, ensure that wineprefix is up to date with wineboot.
|
||||
// Wait for this separately from the first frame, as setting up
|
||||
// a new prefix might take 5s on its own.
|
||||
env := []string{
|
||||
"DISPLAY=" + d.display,
|
||||
"WINEDEBUG=fixme-all", // hide "fixme" noise
|
||||
"WINEPREFIX=" + wineprefix,
|
||||
|
||||
// Disable wine-gecko (Explorer) and wine-mono (.NET).
|
||||
// Otherwise, if not installed, wineboot will get stuck
|
||||
// with a prompt to install them on the virtual X
|
||||
// display. Moreover, Gio doesn't need either, and wine
|
||||
// is faster without them.
|
||||
"WINEDLLOVERRIDES=mscoree,mshtml=",
|
||||
}
|
||||
{
|
||||
start := time.Now()
|
||||
cmd := exec.Command("wine", "wineboot", "-i")
|
||||
cmd.Env = env
|
||||
// Use a combined output pipe instead of CombinedOutput,
|
||||
// so that we only wait for the child process to exit,
|
||||
// and we don't need to wait for all of wine's
|
||||
// grandchildren to exit and stop writing. This is
|
||||
// relevant as wine leaves "wineserver" lingering for
|
||||
// three seconds by default, to be reused later.
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
d.Fatal(err)
|
||||
}
|
||||
cmd.Stderr = cmd.Stdout
|
||||
if err := cmd.Run(); err != nil {
|
||||
io.Copy(os.Stderr, stdout)
|
||||
d.Fatal(err)
|
||||
}
|
||||
d.Logf("set up WINEPREFIX in %s", time.Since(start))
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cmd := exec.CommandContext(ctx, "wine", bin)
|
||||
cmd.Env = env
|
||||
output, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
d.Fatal(err)
|
||||
}
|
||||
cmd.Stderr = cmd.Stdout
|
||||
d.output = output
|
||||
if err := cmd.Start(); err != nil {
|
||||
d.Fatal(err)
|
||||
}
|
||||
d.Cleanup(cancel)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
if err := cmd.Wait(); err != nil && ctx.Err() == nil {
|
||||
d.Error(err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
// Wait for the gio app to render.
|
||||
d.waitForFrame()
|
||||
|
||||
// xdotool seems to fail at actually moving the window if we use it
|
||||
// immediately after Gio is ready. Why?
|
||||
// We can't tell if the windowmove operation worked until we take a
|
||||
// screenshot, because the getwindowgeometry op reports the 0x0
|
||||
// coordinates even if the window wasn't moved properly.
|
||||
// A sleep of ~20ms seems to be enough on an idle laptop. Use 20x that.
|
||||
// TODO(mvdan): revisit this, when you have a spare three hours.
|
||||
time.Sleep(400 * time.Millisecond)
|
||||
id := d.xdotool("search", "--sync", "--onlyvisible", "--name", "Gio")
|
||||
d.xdotool("windowmove", "--sync", id, 0, 0)
|
||||
}
|
||||
|
||||
func (d *WineTestDriver) Screenshot() image.Image {
|
||||
img := d.X11TestDriver.Screenshot()
|
||||
// Crop the screenshot back to the original dimensions.
|
||||
cropped := image.NewRGBA(image.Rect(0, 0, d.width, d.height))
|
||||
draw.Draw(cropped, cropped.Bounds(), img, image.Point{}, draw.Src)
|
||||
return cropped
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image/png"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/akavel/rsrc/binutil"
|
||||
"github.com/akavel/rsrc/coff"
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
)
|
||||
|
||||
func buildWindows(tmpDir string, bi *buildInfo) error {
|
||||
builder := &windowsBuilder{TempDir: tmpDir}
|
||||
builder.DestDir = *destPath
|
||||
if builder.DestDir == "" {
|
||||
builder.DestDir = bi.pkgPath
|
||||
}
|
||||
|
||||
name := bi.name
|
||||
if *destPath != "" {
|
||||
if filepath.Ext(*destPath) != ".exe" {
|
||||
return fmt.Errorf("invalid output name %q, it must end with `.exe`", *destPath)
|
||||
}
|
||||
name = filepath.Base(*destPath)
|
||||
}
|
||||
name = strings.TrimSuffix(name, ".exe")
|
||||
sdk := bi.minsdk
|
||||
if sdk > 10 {
|
||||
return fmt.Errorf("invalid minsdk (%d) it's higher than Windows 10", sdk)
|
||||
}
|
||||
version := strconv.Itoa(bi.version)
|
||||
if bi.version > math.MaxUint16 {
|
||||
return fmt.Errorf("version (%d) is larger than the maximum (%d)", bi.version, math.MaxUint16)
|
||||
}
|
||||
|
||||
for _, arch := range bi.archs {
|
||||
builder.Coff = coff.NewRSRC()
|
||||
builder.Coff.Arch(arch)
|
||||
|
||||
if err := builder.embedIcon(bi.iconPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := builder.embedManifest(windowsManifest{
|
||||
Version: "1.0.0." + version,
|
||||
WindowsVersion: sdk,
|
||||
Name: name,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("can't create manifest: %v", err)
|
||||
}
|
||||
|
||||
if err := builder.embedInfo(windowsResources{
|
||||
Version: [2]uint32{uint32(1) << 16, uint32(bi.version)},
|
||||
VersionHuman: "1.0.0." + version,
|
||||
Name: name,
|
||||
Language: 0x0400, // Process Default Language: https://docs.microsoft.com/en-us/previous-versions/ms957130(v=msdn.10)
|
||||
}); err != nil {
|
||||
return fmt.Errorf("can't create info: %v", err)
|
||||
}
|
||||
|
||||
if err := builder.buildResource(bi, name, arch); err != nil {
|
||||
return fmt.Errorf("can't build the resources: %v", err)
|
||||
}
|
||||
|
||||
if err := builder.buildProgram(bi, name, arch); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type (
|
||||
windowsResources struct {
|
||||
Version [2]uint32
|
||||
VersionHuman string
|
||||
Language uint16
|
||||
Name string
|
||||
}
|
||||
windowsManifest struct {
|
||||
Version string
|
||||
WindowsVersion int
|
||||
Name string
|
||||
}
|
||||
windowsBuilder struct {
|
||||
TempDir string
|
||||
DestDir string
|
||||
Coff *coff.Coff
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
// https://docs.microsoft.com/en-us/windows/win32/menurc/resource-types
|
||||
windowsResourceIcon = 3
|
||||
windowsResourceIconGroup = windowsResourceIcon + 11
|
||||
windowsResourceManifest = 24
|
||||
windowsResourceVersion = 16
|
||||
)
|
||||
|
||||
type bufferCoff struct {
|
||||
bytes.Buffer
|
||||
}
|
||||
|
||||
func (b *bufferCoff) Size() int64 {
|
||||
return int64(b.Len())
|
||||
}
|
||||
|
||||
func (b *windowsBuilder) embedIcon(path string) (err error) {
|
||||
iconFile, err := os.Open(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("can't read the icon located at %s: %v", path, err)
|
||||
}
|
||||
defer iconFile.Close()
|
||||
|
||||
iconImage, err := png.Decode(iconFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't decode the PNG file (%s): %v", path, err)
|
||||
}
|
||||
|
||||
sizes := []int{16, 32, 48, 64, 128, 256}
|
||||
var iconHeader bufferCoff
|
||||
|
||||
// GRPICONDIR structure.
|
||||
if err := binary.Write(&iconHeader, binary.LittleEndian, [3]uint16{0, 1, uint16(len(sizes))}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, size := range sizes {
|
||||
var iconBuffer bufferCoff
|
||||
|
||||
if err := png.Encode(&iconBuffer, resizeIcon(iconVariant{size: size, fill: false}, iconImage)); err != nil {
|
||||
return fmt.Errorf("can't encode image: %v", err)
|
||||
}
|
||||
|
||||
b.Coff.AddResource(windowsResourceIcon, uint16(size), &iconBuffer)
|
||||
|
||||
if err := binary.Write(&iconHeader, binary.LittleEndian, struct {
|
||||
Size [2]uint8
|
||||
Color [2]uint8
|
||||
Planes uint16
|
||||
BitCount uint16
|
||||
Length uint32
|
||||
Id uint16
|
||||
}{
|
||||
Size: [2]uint8{uint8(size % 256), uint8(size % 256)}, // "0" means 256px.
|
||||
Planes: 1,
|
||||
BitCount: 32,
|
||||
Length: uint32(iconBuffer.Len()),
|
||||
Id: uint16(size),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
b.Coff.AddResource(windowsResourceIconGroup, 1, &iconHeader)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *windowsBuilder) buildResource(buildInfo *buildInfo, name string, arch string) error {
|
||||
out, err := os.Create(filepath.Join(buildInfo.pkgPath, name+"_windows_"+arch+".syso"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
b.Coff.Freeze()
|
||||
|
||||
// See https://github.com/akavel/rsrc/internal/write.go#L13.
|
||||
w := binutil.Writer{W: out}
|
||||
binutil.Walk(b.Coff, func(v reflect.Value, path string) error {
|
||||
if binutil.Plain(v.Kind()) {
|
||||
w.WriteLE(v.Interface())
|
||||
return nil
|
||||
}
|
||||
vv, ok := v.Interface().(binutil.SizedReader)
|
||||
if ok {
|
||||
w.WriteFromSized(vv)
|
||||
return binutil.WALK_SKIP
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if w.Err != nil {
|
||||
return fmt.Errorf("error writing output file: %s", w.Err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *windowsBuilder) buildProgram(buildInfo *buildInfo, name string, arch string) error {
|
||||
dest := b.DestDir
|
||||
if len(buildInfo.archs) > 1 {
|
||||
dest = filepath.Join(filepath.Dir(b.DestDir), name+"_"+arch+".exe")
|
||||
}
|
||||
|
||||
cmd := exec.Command(
|
||||
"go",
|
||||
"build",
|
||||
"-ldflags=-H=windowsgui "+buildInfo.ldflags,
|
||||
"-tags="+buildInfo.tags,
|
||||
"-o", dest,
|
||||
buildInfo.pkgPath,
|
||||
)
|
||||
cmd.Env = append(
|
||||
os.Environ(),
|
||||
"GOOS=windows",
|
||||
"GOARCH="+arch,
|
||||
)
|
||||
_, err := runCmd(cmd)
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *windowsBuilder) embedManifest(v windowsManifest) error {
|
||||
t, err := template.New("manifest").Parse(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<assemblyIdentity type="win32" name="{{.Name}}" version="{{.Version}}" />
|
||||
<description>{{.Name}}</description>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
{{if (le .WindowsVersion 10)}}<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
{{end}}
|
||||
{{if (le .WindowsVersion 9)}}<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
|
||||
{{end}}
|
||||
{{if (le .WindowsVersion 8)}}<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
|
||||
{{end}}
|
||||
{{if (le .WindowsVersion 7)}}<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
|
||||
{{end}}
|
||||
{{if (le .WindowsVersion 6)}}<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
|
||||
{{end}}
|
||||
</application>
|
||||
</compatibility>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
</assembly>`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var manifest bufferCoff
|
||||
if err := t.Execute(&manifest, v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.Coff.AddResource(windowsResourceManifest, 1, &manifest)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *windowsBuilder) embedInfo(v windowsResources) error {
|
||||
page := uint16(1)
|
||||
|
||||
// https://docs.microsoft.com/pt-br/windows/win32/menurc/vs-versioninfo
|
||||
t := newValue(valueBinary, "VS_VERSION_INFO", []io.WriterTo{
|
||||
// https://docs.microsoft.com/pt-br/windows/win32/api/VerRsrc/ns-verrsrc-vs_fixedfileinfo
|
||||
windowsInfoValueFixed{
|
||||
Signature: 0xFEEF04BD,
|
||||
StructVersion: 0x00010000,
|
||||
FileVersion: v.Version,
|
||||
ProductVersion: v.Version,
|
||||
FileFlagMask: 0x3F,
|
||||
FileFlags: 0,
|
||||
FileOS: 0x40004,
|
||||
FileType: 0x1,
|
||||
FileSubType: 0,
|
||||
},
|
||||
// https://docs.microsoft.com/pt-br/windows/win32/menurc/stringfileinfo
|
||||
newValue(valueText, "StringFileInfo", []io.WriterTo{
|
||||
// https://docs.microsoft.com/pt-br/windows/win32/menurc/stringtable
|
||||
newValue(valueText, fmt.Sprintf("%04X%04X", v.Language, page), []io.WriterTo{
|
||||
// https://docs.microsoft.com/pt-br/windows/win32/menurc/string-str
|
||||
newValue(valueText, "ProductVersion", v.VersionHuman),
|
||||
newValue(valueText, "FileVersion", v.VersionHuman),
|
||||
newValue(valueText, "FileDescription", v.Name),
|
||||
newValue(valueText, "ProductName", v.Name),
|
||||
// TODO include more data: gogio must have some way to provide such information (like Company Name, Copyright...)
|
||||
}),
|
||||
}),
|
||||
// https://docs.microsoft.com/pt-br/windows/win32/menurc/varfileinfo
|
||||
newValue(valueBinary, "VarFileInfo", []io.WriterTo{
|
||||
// https://docs.microsoft.com/pt-br/windows/win32/menurc/var-str
|
||||
newValue(valueBinary, "Translation", uint32(page)<<16|uint32(v.Language)),
|
||||
}),
|
||||
})
|
||||
|
||||
// For some reason the ValueLength of the VS_VERSIONINFO must be the byte-length of `windowsInfoValueFixed`:
|
||||
t.ValueLength = 52
|
||||
|
||||
var verrsrc bufferCoff
|
||||
if _, err := t.WriteTo(&verrsrc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.Coff.AddResource(windowsResourceVersion, 1, &verrsrc)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type windowsInfoValueFixed struct {
|
||||
Signature uint32
|
||||
StructVersion uint32
|
||||
FileVersion [2]uint32
|
||||
ProductVersion [2]uint32
|
||||
FileFlagMask uint32
|
||||
FileFlags uint32
|
||||
FileOS uint32
|
||||
FileType uint32
|
||||
FileSubType uint32
|
||||
FileDate [2]uint32
|
||||
}
|
||||
|
||||
func (v windowsInfoValueFixed) WriteTo(w io.Writer) (_ int64, err error) {
|
||||
return 0, binary.Write(w, binary.LittleEndian, v)
|
||||
}
|
||||
|
||||
type windowsInfoValue struct {
|
||||
Length uint16
|
||||
ValueLength uint16
|
||||
Type uint16
|
||||
Key []byte
|
||||
Value []byte
|
||||
}
|
||||
|
||||
func (v windowsInfoValue) WriteTo(w io.Writer) (_ int64, err error) {
|
||||
// binary.Write doesn't support []byte inside struct.
|
||||
if err = binary.Write(w, binary.LittleEndian, [3]uint16{v.Length, v.ValueLength, v.Type}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if _, err = w.Write(v.Key); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if _, err = w.Write(v.Value); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
const (
|
||||
valueBinary uint16 = 0
|
||||
valueText uint16 = 1
|
||||
)
|
||||
|
||||
func newValue(valueType uint16, key string, input interface{}) windowsInfoValue {
|
||||
v := windowsInfoValue{
|
||||
Type: valueType,
|
||||
Length: 6,
|
||||
}
|
||||
|
||||
padding := func(in []byte) []byte {
|
||||
if l := uint16(len(in)) + v.Length; l%4 != 0 {
|
||||
return append(in, make([]byte, 4-l%4)...)
|
||||
}
|
||||
return in
|
||||
}
|
||||
|
||||
v.Key = padding(utf16Encode(key))
|
||||
v.Length += uint16(len(v.Key))
|
||||
|
||||
switch in := input.(type) {
|
||||
case string:
|
||||
v.Value = padding(utf16Encode(in))
|
||||
v.ValueLength = uint16(len(v.Value) / 2)
|
||||
case []io.WriterTo:
|
||||
var buff bytes.Buffer
|
||||
for k := range in {
|
||||
if _, err := in[k].WriteTo(&buff); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
v.Value = buff.Bytes()
|
||||
default:
|
||||
var buff bytes.Buffer
|
||||
if err := binary.Write(&buff, binary.LittleEndian, in); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
v.ValueLength = uint16(buff.Len())
|
||||
v.Value = buff.Bytes()
|
||||
}
|
||||
|
||||
v.Length += uint16(len(v.Value))
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
// utf16Encode encodes the string to UTF16 with null-termination.
|
||||
func utf16Encode(s string) []byte {
|
||||
b, err := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder().Bytes([]byte(s))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return append(b, 0x00, 0x00) // null-termination.
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
"io"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type X11TestDriver struct {
|
||||
driverBase
|
||||
|
||||
display string
|
||||
}
|
||||
|
||||
func (d *X11TestDriver) Start(path string) {
|
||||
// First, build the app.
|
||||
bin := filepath.Join(d.tempDir("gio-endtoend-x11"), "red")
|
||||
flags := []string{"build", "-tags", "nowayland", "-o=" + bin}
|
||||
if raceEnabled {
|
||||
flags = append(flags, "-race")
|
||||
}
|
||||
flags = append(flags, path)
|
||||
cmd := exec.Command("go", flags...)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
d.Fatalf("could not build app: %s:\n%s", err, out)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
d.Cleanup(wg.Wait)
|
||||
|
||||
d.startServer(&wg, d.width, d.height)
|
||||
|
||||
// Then, start our program on the X server above.
|
||||
{
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cmd := exec.CommandContext(ctx, bin)
|
||||
cmd.Env = []string{"DISPLAY=" + d.display}
|
||||
output, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
d.Fatal(err)
|
||||
}
|
||||
cmd.Stderr = cmd.Stdout
|
||||
d.output = output
|
||||
if err := cmd.Start(); err != nil {
|
||||
d.Fatal(err)
|
||||
}
|
||||
d.Cleanup(cancel)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
if err := cmd.Wait(); err != nil && ctx.Err() == nil {
|
||||
d.Error(err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait for the gio app to render.
|
||||
d.waitForFrame()
|
||||
}
|
||||
|
||||
func (d *X11TestDriver) startServer(wg *sync.WaitGroup, width, height int) {
|
||||
// Pick a random display number between 1 and 100,000. Most machines
|
||||
// will only be using :0, so there's only a 0.001% chance of two
|
||||
// concurrent test runs to run into a conflict.
|
||||
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
d.display = fmt.Sprintf(":%d", rnd.Intn(100000)+1)
|
||||
|
||||
var xprog string
|
||||
xflags := []string{
|
||||
"-wr", // we want a white background; the default is black
|
||||
}
|
||||
if *headless {
|
||||
xprog = "Xvfb" // virtual X server
|
||||
xflags = append(xflags, "-screen", "0", fmt.Sprintf("%dx%dx24", width, height))
|
||||
} else {
|
||||
xprog = "Xephyr" // nested X server as a window
|
||||
xflags = append(xflags, "-screen", fmt.Sprintf("%dx%d", width, height))
|
||||
}
|
||||
xflags = append(xflags, d.display)
|
||||
|
||||
d.needPrograms(
|
||||
xprog, // to run the X server
|
||||
"scrot", // to take screenshots
|
||||
"xdotool", // to send input
|
||||
)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cmd := exec.CommandContext(ctx, xprog, xflags...)
|
||||
combined := &bytes.Buffer{}
|
||||
cmd.Stdout = combined
|
||||
cmd.Stderr = combined
|
||||
if err := cmd.Start(); err != nil {
|
||||
d.Fatal(err)
|
||||
}
|
||||
d.Cleanup(cancel)
|
||||
d.Cleanup(func() {
|
||||
// Give it a chance to exit gracefully, cleaning up
|
||||
// after itself. After 10ms, the deferred cancel above
|
||||
// will signal an os.Kill.
|
||||
cmd.Process.Signal(os.Interrupt)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
})
|
||||
|
||||
// Wait for the X server to be ready. The socket path isn't
|
||||
// terribly portable, but that's okay for now.
|
||||
withRetries(d.T, time.Second, func() error {
|
||||
socket := fmt.Sprintf("/tmp/.X11-unix/X%s", d.display[1:])
|
||||
_, err := os.Stat(socket)
|
||||
return err
|
||||
})
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
if err := cmd.Wait(); err != nil && ctx.Err() == nil {
|
||||
// Print all output and error.
|
||||
io.Copy(os.Stdout, combined)
|
||||
d.Error(err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
func (d *X11TestDriver) Screenshot() image.Image {
|
||||
cmd := exec.Command("scrot", "--silent", "--overwrite", "/dev/stdout")
|
||||
cmd.Env = []string{"DISPLAY=" + d.display}
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
d.Errorf("%s", out)
|
||||
d.Fatal(err)
|
||||
}
|
||||
img, err := png.Decode(bytes.NewReader(out))
|
||||
if err != nil {
|
||||
d.Fatal(err)
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
func (d *X11TestDriver) xdotool(args ...interface{}) string {
|
||||
d.Helper()
|
||||
strs := make([]string, len(args))
|
||||
for i, arg := range args {
|
||||
strs[i] = fmt.Sprint(arg)
|
||||
}
|
||||
cmd := exec.Command("xdotool", strs...)
|
||||
cmd.Env = []string{"DISPLAY=" + d.display}
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
d.Errorf("%s", out)
|
||||
d.Fatal(err)
|
||||
}
|
||||
return string(bytes.TrimSpace(out))
|
||||
}
|
||||
|
||||
func (d *X11TestDriver) Click(x, y int) {
|
||||
d.xdotool("mousemove", "--sync", x, y)
|
||||
d.xdotool("click", "1")
|
||||
|
||||
// Wait for the gio app to render after this click.
|
||||
d.waitForFrame()
|
||||
}
|
||||
Reference in New Issue
Block a user