all: initial import from gio main repository

Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2022-04-24 13:06:19 +02:00
commit 6826ef0b64
27 changed files with 4487 additions and 0 deletions
+67
View File
@@ -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
+22
View File
@@ -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 ./...
+91
View File
@@ -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
+18
View File
@@ -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 ./...
+63
View File
@@ -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/>
---
+21
View File
@@ -0,0 +1,21 @@
# Gio Tools
Tools for the [Gio project](https://gioui.org), most notably `gogio` for packaging Gio programs.
[![builds.sr.ht status](https://builds.sr.ht/~eliasnaur/gio-cmd.svg)](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.
+31
View File
@@ -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
)
+118
View File
@@ -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=
+143
View File
@@ -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
+156
View File
@@ -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)
}
+32
View File
@@ -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)
}
}
}
+10
View File
@@ -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
+331
View File
@@ -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)
}
}
+69
View File
@@ -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.
`
+589
View File
@@ -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()
}
+123
View File
@@ -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()
}
+201
View File
@@ -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
View File
@@ -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
}
+17
View File
@@ -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())
}
+33
View File
@@ -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"`,
},
}
+8
View File
@@ -0,0 +1,8 @@
// SPDX-License-Identifier: Unlicense OR MIT
//go:build race
// +build race
package main_test
func init() { raceEnabled = true }
+142
View File
@@ -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}
}
+196
View File
@@ -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()
}
+152
View File
@@ -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
}
+416
View File
@@ -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.
}
+170
View File
@@ -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()
}