Merge branch 'master' of https://github.com/highfidelity/hifi into loginInitiative2

This commit is contained in:
Wayne Chen 2018-12-11 20:01:19 -08:00
commit 2f07a0eb5d
74 changed files with 2475 additions and 546 deletions

20
.gitignore vendored
View file

@ -17,10 +17,11 @@ Makefile
local.properties
android/gradle*
android/.gradle
android/app/src/main/jniLibs
android/app/libs
android/app/src/main/res/values/libs.xml
android/app/src/main/assets/bundled
android/**/src/main/jniLibs
android/**/libs
android/**/src/main/res/values/libs.xml
android/**/src/main/assets
android/**/gradle*
# VSCode
# List taken from Github Global Ignores master@435c4d92
@ -83,9 +84,6 @@ npm-debug.log
# Android studio files
*___jb_old___
# Generated assets for Android
android/app/src/main/assets
# Resource binary file
interface/compiledResources
@ -94,3 +92,11 @@ interface/resources/GPUCache/*
# package lock file for JSDoc tool
tools/jsdoc/package-lock.json
# Python compile artifacts
**/__pycache__
# ignore unneeded unity project files for avatar exporter
tools/unity-avatar-exporter/Library
tools/unity-avatar-exporter/Packages
tools/unity-avatar-exporter/ProjectSettings

View file

@ -6,13 +6,8 @@ Building is currently supported on OSX, Windows and Linux platforms, but develop
You will need the following tools to build Android targets.
* [Gradle](https://gradle.org/install/)
* [Android Studio](https://developer.android.com/studio/index.html)
### Gradle
Install gradle version 4.1 or higher. Following the instructions to install via [SDKMAN!](http://sdkman.io/install.html) is recommended.
### Android Studio
Download the Android Studio installer and run it. Once installed, at the welcome screen, click configure in the lower right corner and select SDK manager
@ -29,6 +24,8 @@ From the SDK Tools tab select the following
* Android SDK Tools
* NDK (even if you have the NDK installed separately)
Make sure the NDK installed version is 18 (or higher)
# Environment
Setting up the environment for android builds requires some additional steps
@ -51,17 +48,17 @@ Enter the repository `android` directory
`cd hifi/android`
Execute a gradle pre-build setup. This step should only need to be done once
Execute two gradle pre-build steps. This steps should only need to be done once, unless you're working on the Android dependencies
`gradle setupDependencies`
`./gradlew extractDependencies`
`./gradlew setupDependencies`
# Building & Running
* Open Android Studio
* Choose _Open Existing Android Studio Project_
* Navigate to the `hifi` repository and choose the `android` folder and select _OK_
* If Android Studio asks you if you want to use the Gradle wrapper, select cancel and tell it where your local gradle installation is. If you used SDKMAN to install gradle it will be located in `$HOME/.sdkman/candidates/gradle/current/`
* From the _Build_ menu select _Make Project_
* Once the build completes, from the _Run_ menu select _Run App_

View file

@ -91,19 +91,73 @@ In a server, it does not make sense to compile interface
### Running the software
#### Domain server
Running domain server:
```bash
./domain-server/domain-server
```
#### Assignment clients
Running assignment client:
```bash
./assignment-client/assignment-client -n 6
```
#### Interface
Running interface:
```bash
./interface/interface
```
Go to localhost in the running interface.
##### Ubuntu 18.04 only
In Ubuntu 18.04 there is a problem related with NVidia driver library version.
It can be workarounded following these steps:
Uninstall incompatible nvtt libraries:
```bash
sudo apt-get remove libnvtt2 libnvtt-dev
```
Install libssl1.0-dev:
```bash
sudo apt-get -y install libssl1.0-dev
```
Clone castano nvidia-texture-tools:
```
git clone https://github.com/castano/nvidia-texture-tools
cd nvidia-texture-tools/
```
Make these changes in repo:
* In file **VERSION** set `2.2.1`
* In file **configure**:
* set `build="release"`
* set `-DNVTT_SHARED=1`
Configure, build and install:
```
./configure
make
sudo make install
```
Link compiled files:
```
sudo ln -s /usr/local/lib/libnvcore.so /usr/lib/libnvcore.so
sudo ln -s /usr/local/lib/libnvimage.so /usr/lib/libnvimage.so
sudo ln -s /usr/local/lib/libnvmath.so /usr/lib/libnvmath.so
sudo ln -s /usr/local/lib/libnvtt.so /usr/lib/libnvtt.so
```
After running this steps you can run interface:
```
interface/interface
```

View file

@ -139,23 +139,23 @@ dependencies {
implementation 'com.android.support.constraint:constraint-layout:1.0.2'
implementation 'com.android.support:design:26.1.0'
compile 'com.android.support:support-v4:26.1.0'
compile 'com.android.support:appcompat-v7:26.1.0'
compile 'com.android.support:support-vector-drawable:26.1.0'
api 'com.android.support:support-v4:26.1.0'
api 'com.android.support:appcompat-v7:26.1.0'
api 'com.android.support:support-vector-drawable:26.1.0'
implementation 'com.android.support:appcompat-v7:26.1.0'
compile 'com.android.support:recyclerview-v7:26.1.0'
compile 'com.android.support:cardview-v7:26.1.0'
api 'com.android.support:recyclerview-v7:26.1.0'
api 'com.android.support:cardview-v7:26.1.0'
compile 'com.squareup.retrofit2:retrofit:2.4.0'
compile 'com.squareup.retrofit2:converter-gson:2.4.0'
api 'com.squareup.retrofit2:retrofit:2.4.0'
api 'com.squareup.retrofit2:converter-gson:2.4.0'
implementation 'com.squareup.picasso:picasso:2.71828'
compile 'com.squareup.retrofit2:retrofit:2.4.0'
compile 'com.squareup.retrofit2:converter-gson:2.4.0'
api 'com.squareup.retrofit2:retrofit:2.4.0'
api 'com.squareup.retrofit2:converter-gson:2.4.0'
implementation 'com.squareup.picasso:picasso:2.71828'
compile 'com.sothree.slidinguppanel:library:3.4.0'
api 'com.sothree.slidinguppanel:library:3.4.0'
implementation fileTree(include: ['*.jar'], dir: 'libs')
}

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="io.highfidelity.hifiinterface">
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="26" />
<uses-feature android:glEsVersion="0x00030002" android:required="true" />
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.INTERNET" />

View file

@ -14,7 +14,7 @@ buildscript {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
classpath 'com.android.tools.build:gradle:3.2.1'
}
}
@ -392,7 +392,7 @@ task extractDependencies(dependsOn: verifyDependencyDownloads) {
}
// Copies the non Qt dependencies. Qt dependencies (primary libraries and plugins) are handled by the qtBundle task
task copyDependencies(dependsOn: [ extractDependencies ]) {
task copyDependencies() {
doLast {
packages.each { entry ->
def packageName = entry.key
@ -414,7 +414,7 @@ task copyDependencies(dependsOn: [ extractDependencies ]) {
}
}
task extractGvrBinaries(dependsOn: extractDependencies) {
task extractGvrBinaries() {
doLast {
def gvrLibFolder = new File(HIFI_ANDROID_PRECOMPILED, 'gvr/gvr-android-sdk-1.101.0/libraries');
zipTree(new File(HIFI_ANDROID_PRECOMPILED, 'gvr/gvr-android-sdk-1.101.0/libraries/sdk-audio-1.101.0.aar')).visit { element ->

4
android/build_android.sh Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -xeuo pipefail
./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} setupDependencies
./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} app:${ANDROID_BUILD_TARGET}

22
android/containerized_build.sh Executable file
View file

@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -xeuo pipefail
DOCKER_IMAGE_NAME="hifi_androidbuild"
docker build --build-arg BUILD_UID=`id -u` -t "${DOCKER_IMAGE_NAME}" -f docker/Dockerfile docker
docker run \
--rm \
--security-opt seccomp:unconfined \
-v "${WORKSPACE}":/home/jenkins/hifi \
-e "RELEASE_NUMBER=${RELEASE_NUMBER}" \
-e "RELEASE_TYPE=${RELEASE_TYPE}" \
-e "ANDROID_BUILD_TARGET=assembleDebug" \
-e "CMAKE_BACKTRACE_URL=${CMAKE_BACKTRACE_URL}" \
-e "CMAKE_BACKTRACE_TOKEN=${CMAKE_BACKTRACE_TOKEN}" \
-e "CMAKE_BACKTRACE_SYMBOLS_TOKEN=${CMAKE_BACKTRACE_SYMBOLS_TOKEN}" \
-e "GA_TRACKING_ID=${GA_TRACKING_ID}" \
-e "GIT_PR_COMMIT=${GIT_PR_COMMIT}" \
-e "VERSION_CODE=${VERSION_CODE}" \
"${DOCKER_IMAGE_NAME}" \
sh -c "./build_android.sh"

92
android/docker/Dockerfile Normal file
View file

@ -0,0 +1,92 @@
FROM openjdk:8
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
RUN apt-get update && apt-get -y install \
curl \
gnupg \
software-properties-common \
unzip \
-
# --- Versions and Download paths
ENV ANDROID_HOME="/usr/local/android-sdk" \
ANDROID_NDK_HOME="/usr/local/android-ndk" \
ANDROID_SDK_HOME="/usr/local/android-sdk-home" \
ANDROID_VERSION=26 \
ANDROID_BUILD_TOOLS_VERSION=28.0.3 \
ANDROID_NDK_VERSION=r18
ENV SDK_URL="https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip" \
NDK_URL="https://dl.google.com/android/repository/android-ndk-${ANDROID_NDK_VERSION}-linux-x86_64.zip"
# --- Android SDK
RUN mkdir -p "$ANDROID_HOME" "$ANDROID_SDK_HOME" && \
cd "$ANDROID_HOME" && \
curl -s -S -o sdk.zip -L "${SDK_URL}" && \
unzip sdk.zip && \
rm sdk.zip && \
yes | $ANDROID_HOME/tools/bin/sdkmanager --licenses
# Install Android Build Tool and Libraries
RUN $ANDROID_HOME/tools/bin/sdkmanager --update
RUN $ANDROID_HOME/tools/bin/sdkmanager "build-tools;${ANDROID_BUILD_TOOLS_VERSION}" \
"platforms;android-${ANDROID_VERSION}" \
"platform-tools"
RUN chmod -R a+w "${ANDROID_HOME}"
RUN chmod -R a+w "${ANDROID_SDK_HOME}"
# --- Android NDK
# download
RUN mkdir /usr/local/android-ndk-tmp && \
cd /usr/local/android-ndk-tmp && \
curl -s -S -o ndk.zip -L "${NDK_URL}" && \
unzip -q ndk.zip && \
mv ./android-ndk-${ANDROID_NDK_VERSION} ${ANDROID_NDK_HOME} && \
cd ${ANDROID_NDK_HOME} && \
rm -rf /usr/local/android-ndk-tmp
ENV PATH ${PATH}:${ANDROID_NDK_HOME}
RUN apt-get -y install \
g++ \
gcc \
-
# --- Gradle
ARG BUILD_UID=1001
RUN useradd -ms /bin/bash -u $BUILD_UID jenkins
USER jenkins
WORKDIR /home/jenkins
# Hifi dependencies
ENV HIFI_BASE="/home/jenkins/hifi_android"
ENV HIFI_ANDROID_PRECOMPILED="$HIFI_BASE/dependencies"
ENV HIFI_VCPKG_BASE="$HIFI_BASE/vcpkg"
RUN mkdir "$HIFI_BASE" && \
mkdir "$HIFI_VCPKG_BASE" && \
mkdir "$HIFI_ANDROID_PRECOMPILED"
RUN git clone https://github.com/jherico/hifi.git && \
cd ~/hifi && \
git checkout feature/build/gradle-wrapper
WORKDIR /home/jenkins/hifi
RUN touch .test4 && \
git fetch && git reset origin/feature/build/gradle-wrapper --hard
RUN mkdir build
# Pre-cache the vcpkg managed dependencies
WORKDIR /home/jenkins/hifi/build
RUN python3 ../prebuild.py --build-root `pwd` --android
# Pre-cache the gradle dependencies
WORKDIR /home/jenkins/hifi/android
RUN ./gradlew -m tasks -PHIFI_ANDROID_PRECOMPILED=$HIFI_ANDROID_PRECOMPILED
RUN ./gradlew extractDependencies -PHIFI_ANDROID_PRECOMPILED=$HIFI_ANDROID_PRECOMPILED

Binary file not shown.

View file

@ -0,0 +1,6 @@
#Sat Dec 01 08:32:47 PST 2018
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip

172
android/gradlew vendored Executable file
View file

@ -0,0 +1,172 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

84
android/gradlew.bat vendored Executable file
View file

@ -0,0 +1,84 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View file

@ -29,7 +29,7 @@ ScriptableAvatar::ScriptableAvatar() {
QByteArray ScriptableAvatar::toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking) {
_globalPosition = getWorldPosition();
return AvatarData::toByteArrayStateful(dataDetail);
return AvatarData::toByteArrayStateful(dataDetail, dropFaceTracking);
}

286
hifi_android.py Normal file
View file

@ -0,0 +1,286 @@
import hifi_utils
import json
import os
import platform
import re
import shutil
import xml.etree.ElementTree as ET
import functools
print = functools.partial(print, flush=True)
ANDROID_PACKAGE_URL = 'https://hifi-public.s3.amazonaws.com/dependencies/android/'
ANDROID_PACKAGES = {
'qt' : {
'file': 'qt-5.11.1_linux_armv8-libcpp_openssl_patched.tgz',
'versionId': '3S97HBM5G5Xw9EfE52sikmgdN3t6C2MN',
'checksum': 'aa449d4bfa963f3bc9a9dfe558ba29df',
},
'bullet': {
'file': 'bullet-2.88_armv8-libcpp.tgz',
'versionId': 'S8YaoED0Cl8sSb8fSV7Q2G1lQJSNDxqg',
'checksum': '81642779ccb110f8c7338e8739ac38a0',
},
'draco': {
'file': 'draco_armv8-libcpp.tgz',
'versionId': '3.B.uBj31kWlgND3_R2xwQzT_TP6Dz_8',
'checksum': '617a80d213a5ec69fbfa21a1f2f738cd',
},
'glad': {
'file': 'glad_armv8-libcpp.zip',
'versionId': 'r5Zran.JSCtvrrB6Q4KaqfIoALPw3lYY',
'checksum': 'a8ee8584cf1ccd34766c7ddd9d5e5449',
},
'gvr': {
'file': 'gvrsdk_v1.101.0.tgz',
'versionId': 'nqBV_j81Uc31rC7bKIrlya_Hah4v3y5r',
'checksum': '57fd02baa069176ba18597a29b6b4fc7',
},
'nvtt': {
'file': 'nvtt_armv8-libcpp.zip',
'versionId': 'lmkBVR5t4UF1UUwMwEirnk9H_8Nt90IO',
'checksum': 'eb46d0b683e66987190ed124aabf8910',
'sharedLibFolder': 'lib',
'includeLibs': ['libnvtt.so', 'libnvmath.so', 'libnvimage.so', 'libnvcore.so']
},
'oculus': {
'file': 'ovr_sdk_mobile_1.19.0.zip',
'versionId': 's_RN1vlEvUi3pnT7WPxUC4pQ0RJBs27y',
'checksum': '98f0afb62861f1f02dd8110b31ed30eb',
'sharedLibFolder': 'VrApi/Libs/Android/arm64-v8a/Release',
'includeLibs': ['libvrapi.so']
},
'openssl': {
'file': 'openssl-1.1.0g_armv8.tgz',
'versionId': 'AiiPjmgUZTgNj7YV1EEx2lL47aDvvvAW',
'checksum': 'cabb681fbccd79594f65fcc266e02f32'
},
'polyvox': {
'file': 'polyvox_armv8-libcpp.tgz',
'versionId': 'A2kbKiNhpIenGq23bKRRzg7IMAI5BI92',
'checksum': 'dba88b3a098747af4bb169e9eb9af57e',
'sharedLibFolder': 'lib',
'includeLibs': ['Release/libPolyVoxCore.so', 'libPolyVoxUtil.so'],
},
'tbb': {
'file': 'tbb-2018_U1_armv8_libcpp.tgz',
'versionId': 'mrRbWnv4O4evcM1quRH43RJqimlRtaKB',
'checksum': '20768f298f53b195e71b414b0ae240c4',
'sharedLibFolder': 'lib/release',
'includeLibs': ['libtbb.so', 'libtbbmalloc.so'],
},
'hifiAC': {
'baseUrl': 'http://s3.amazonaws.com/hifi-public/dependencies/',
'file': 'codecSDK-android_armv8-2.0.zip',
'checksum': '1cbef929675818fc64c4101b72f84a6a'
},
'etc2comp': {
'file': 'etc2comp-patched-armv8-libcpp.tgz',
'versionId': 'bHhGECRAQR1vkpshBcK6ByNc1BQIM8gU',
'checksum': '14b02795d774457a33bbc60e00a786bc'
},
'breakpad': {
'file': 'breakpad.tgz',
'versionId': '8VrYXz7oyc.QBxNia0BVJOUBvrFO61jI',
'checksum': 'ddcb23df336b08017042ba4786db1d9e',
'sharedLibFolder': 'lib',
'includeLibs': {'libbreakpad_client.a'}
}
}
ANDROID_PLATFORM_PACKAGES = {
'Darwin' : {
'qt': {
'file': 'qt-5.11.1_osx_armv8-libcpp_openssl_patched.tgz',
'versionId': 'OxBD7iKINv1HbyOXmAmDrBb8AF3N.Kup',
'checksum': 'c83cc477c08a892e00c71764dca051a0'
},
},
'Windows' : {
'qt': {
'file': 'qt-5.11.1_win_armv8-libcpp_openssl_patched.tgz',
'versionId': 'JfWM0P_Mz5Qp0LwpzhrsRwN3fqlLSFeT',
'checksum': '0582191cc55431aa4f660848a542883e'
},
}
}
QT5_DEPS = [
'Qt5Concurrent',
'Qt5Core',
'Qt5Gui',
'Qt5Multimedia',
'Qt5Network',
'Qt5OpenGL',
'Qt5Qml',
'Qt5Quick',
'Qt5QuickControls2',
'Qt5QuickTemplates2',
'Qt5Script',
'Qt5ScriptTools',
'Qt5Svg',
'Qt5WebChannel',
'Qt5WebSockets',
'Qt5Widgets',
'Qt5XmlPatterns',
# Android specific
'Qt5AndroidExtras',
'Qt5WebView',
]
def getPlatformPackages():
result = ANDROID_PACKAGES.copy()
system = platform.system()
if system in ANDROID_PLATFORM_PACKAGES:
platformPackages = ANDROID_PLATFORM_PACKAGES[system]
result = { **result, **platformPackages }
return result
def getPackageUrl(package):
url = ANDROID_PACKAGE_URL
if 'baseUrl' in package:
url = package['baseUrl']
url += package['file']
if 'versionId' in package:
url += '?versionId=' + package['versionId']
return url
def copyAndroidLibs(packagePath, appPath):
androidPackages = getPlatformPackages()
jniPath = os.path.join(appPath, 'src/main/jniLibs/arm64-v8a')
if not os.path.isdir(jniPath):
os.makedirs(jniPath)
for packageName in androidPackages:
package = androidPackages[packageName]
if 'sharedLibFolder' in package:
sharedLibFolder = os.path.join(packagePath, packageName, package['sharedLibFolder'])
if 'includeLibs' in package:
for lib in package['includeLibs']:
sourceFile = os.path.join(sharedLibFolder, lib)
destFile = os.path.join(jniPath, os.path.split(lib)[1])
if not os.path.isfile(destFile):
print("Copying {}".format(lib))
shutil.copy(sourceFile, destFile)
class QtPackager:
def __init__(self, appPath, qtRootPath):
self.appPath = appPath
self.qtRootPath = qtRootPath
self.jniPath = os.path.join(self.appPath, 'src/main/jniLibs/arm64-v8a')
self.assetPath = os.path.join(self.appPath, 'src/main/assets')
self.qtAssetPath = os.path.join(self.assetPath, '--Added-by-androiddeployqt--')
# Jars go into the qt library
self.jarPath = os.path.realpath(os.path.join(self.appPath, '../../libraries/qt/libs'))
self.xmlFile = os.path.join(self.appPath, 'src/main/res/values/libs.xml')
self.files = []
self.features = []
self.permissions = []
def copyQtDeps(self):
for lib in QT5_DEPS:
libfile = os.path.join(self.qtRootPath, "lib/lib{}.so".format(lib))
if not os.path.exists(libfile):
continue
self.files.append(libfile)
androidDeps = os.path.join(self.qtRootPath, "lib/{}-android-dependencies.xml".format(lib))
if not os.path.exists(androidDeps):
continue
tree = ET.parse(androidDeps)
root = tree.getroot()
for item in root.findall('./dependencies/lib/depends/*'):
if (item.tag == 'lib') or (item.tag == 'bundled'):
relativeFilename = item.attrib['file']
if (relativeFilename.startswith('qml')):
continue
filename = os.path.join(self.qtRootPath, relativeFilename)
self.files.extend(hifi_utils.recursiveFileList(filename))
elif item.tag == 'jar' and 'bundling' in item.attrib and item.attrib['bundling'] == "1":
self.files.append(os.path.join(self.qtRootPath, item.attrib['file']))
elif item.tag == 'permission':
self.permissions.append(item.attrib['name'])
elif item.tag == 'feature':
self.features.append(item.attrib['name'])
def scanQmlImports(self):
qmlImportCommandFile = os.path.join(self.qtRootPath, 'bin/qmlimportscanner')
system = platform.system()
if 'Windows' == system:
qmlImportCommandFile += ".exe"
if not os.path.isfile(qmlImportCommandFile):
raise RuntimeError("Couldn't find qml import scanner")
qmlRootPath = hifi_utils.scriptRelative('interface/resources/qml')
qmlImportPath = os.path.join(self.qtRootPath, 'qml')
commandResult = hifi_utils.executeSubprocessCapture([
qmlImportCommandFile,
'-rootPath', qmlRootPath,
'-importPath', qmlImportPath
])
qmlImportResults = json.loads(commandResult)
for item in qmlImportResults:
if 'path' not in item:
print("Warning: QML import could not be resolved in any of the import paths: {}".format(item['name']))
continue
path = os.path.realpath(item['path'])
if not os.path.exists(path):
continue
basePath = path
if os.path.isfile(basePath):
basePath = os.path.dirname(basePath)
basePath = os.path.normcase(basePath)
if basePath.startswith(qmlRootPath):
continue
self.files.extend(hifi_utils.recursiveFileList(path))
def processFiles(self):
self.files = list(set(self.files))
self.files.sort()
libsXmlRoot = ET.Element('resources')
qtLibsNode = ET.SubElement(libsXmlRoot, 'array', {'name':'qt_libs'})
bundledLibsNode = ET.SubElement(libsXmlRoot, 'array', {'name':'bundled_in_lib'})
bundledAssetsNode = ET.SubElement(libsXmlRoot, 'array', {'name':'bundled_in_assets'})
libPrefix = 'lib'
for sourceFile in self.files:
if not os.path.isfile(sourceFile):
raise RuntimeError("Unable to find dependency file " + sourceFile)
relativePath = os.path.relpath(sourceFile, self.qtRootPath)
destinationFile = None
if relativePath.endswith('.so'):
garbledFileName = None
if relativePath.startswith(libPrefix):
garbledFileName = relativePath[4:]
p = re.compile(r'lib(Qt5.*).so')
m = p.search(garbledFileName)
if not m:
raise RuntimeError("Huh?")
libName = m.group(1)
ET.SubElement(qtLibsNode, 'item').text = libName
else:
garbledFileName = 'lib' + relativePath.replace('\\', '_'[0])
value = "{}:{}".format(garbledFileName, relativePath).replace('\\', '/')
ET.SubElement(bundledLibsNode, 'item').text = value
destinationFile = os.path.join(self.jniPath, garbledFileName)
elif relativePath.startswith('jar'):
destinationFile = os.path.join(self.jarPath, relativePath[4:])
else:
value = "--Added-by-androiddeployqt--/{}:{}".format(relativePath,relativePath).replace('\\', '/')
ET.SubElement(bundledAssetsNode, 'item').text = value
destinationFile = os.path.join(self.qtAssetPath, relativePath)
destinationParent = os.path.realpath(os.path.dirname(destinationFile))
if not os.path.isdir(destinationParent):
os.makedirs(destinationParent)
if not os.path.isfile(destinationFile):
shutil.copy(sourceFile, destinationFile)
tree = ET.ElementTree(libsXmlRoot)
tree.write(self.xmlFile, 'UTF-8', True)
def bundle(self):
if not os.path.isfile(self.xmlFile) or True:
self.copyQtDeps()
self.scanQmlImports()
self.processFiles()

46
hifi_singleton.py Normal file
View file

@ -0,0 +1,46 @@
import os
import platform
import time
try:
import fcntl
except ImportError:
fcntl = None
# Used to ensure only one instance of the script runs at a time
class Singleton:
def __init__(self, path):
self.fh = None
self.windows = 'Windows' == platform.system()
self.path = path
def __enter__(self):
success = False
while not success:
try:
if self.windows:
if os.path.exists(self.path):
os.unlink(self.path)
self.fh = os.open(self.path, os.O_CREAT | os.O_EXCL | os.O_RDWR)
else:
self.fh = open(self.path, 'x')
fcntl.lockf(self.fh, fcntl.LOCK_EX | fcntl.LOCK_NB)
success = True
except EnvironmentError as err:
if self.fh is not None:
if self.windows:
os.close(self.fh)
else:
self.fh.close()
self.fh = None
print("Couldn't aquire lock, retrying in 10 seconds")
time.sleep(10)
return self
def __exit__(self, type, value, traceback):
if self.windows:
os.close(self.fh)
else:
fcntl.lockf(self.fh, fcntl.LOCK_UN)
self.fh.close()
os.unlink(self.path)

124
hifi_utils.py Normal file
View file

@ -0,0 +1,124 @@
import os
import hashlib
import platform
import shutil
import ssl
import subprocess
import sys
import tarfile
import urllib
import urllib.request
import zipfile
import tempfile
import time
import functools
print = functools.partial(print, flush=True)
def scriptRelative(*paths):
scriptdir = os.path.dirname(os.path.realpath(sys.argv[0]))
result = os.path.join(scriptdir, *paths)
result = os.path.realpath(result)
result = os.path.normcase(result)
return result
def recursiveFileList(startPath):
result = []
if os.path.isfile(startPath):
result.append(startPath)
elif os.path.isdir(startPath):
for dirName, subdirList, fileList in os.walk(startPath):
for fname in fileList:
result.append(os.path.realpath(os.path.join(startPath, dirName, fname)))
result.sort()
return result
def executeSubprocessCapture(processArgs):
processResult = subprocess.run(processArgs, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if (0 != processResult.returncode):
raise RuntimeError('Call to "{}" failed.\n\narguments:\n{}\n\nstdout:\n{}\n\nstderr:\n{}'.format(
processArgs[0],
' '.join(processArgs[1:]),
processResult.stdout.decode('utf-8'),
processResult.stderr.decode('utf-8')))
return processResult.stdout.decode('utf-8')
def executeSubprocess(processArgs, folder=None, env=None):
restoreDir = None
if folder != None:
restoreDir = os.getcwd()
os.chdir(folder)
process = subprocess.Popen(
processArgs, stdout=sys.stdout, stderr=sys.stderr, env=env)
process.wait()
if (0 != process.returncode):
raise RuntimeError('Call to "{}" failed.\n\narguments:\n{}\n'.format(
processArgs[0],
' '.join(processArgs[1:]),
))
if restoreDir != None:
os.chdir(restoreDir)
def hashFile(file, hasher = hashlib.sha512()):
with open(file, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hasher.update(chunk)
return hasher.hexdigest()
# Assumes input files are in deterministic order
def hashFiles(filenames):
hasher = hashlib.sha256()
for filename in filenames:
with open(filename, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hasher.update(chunk)
return hasher.hexdigest()
def hashFolder(folder):
filenames = recursiveFileList(folder)
return hashFiles(filenames)
def downloadFile(url, hash=None, hasher=hashlib.sha512(), retries=3):
for i in range(retries):
tempFileName = None
# OSX Python doesn't support SSL, so we need to bypass it.
# However, we still validate the downloaded file's sha512 hash
if 'Darwin' == platform.system():
tempFileDescriptor, tempFileName = tempfile.mkstemp()
context = ssl._create_unverified_context()
with urllib.request.urlopen(url, context=context) as response, open(tempFileDescriptor, 'wb') as tempFile:
shutil.copyfileobj(response, tempFile)
else:
tempFileName, headers = urllib.request.urlretrieve(url)
# for some reason the hash we get back from the downloaded file is sometimes wrong if we check it right away
# but if we examine the file later, it is correct.
time.sleep(3)
downloadHash = hashFile(tempFileName, hasher)
# Verify the hash
if hash is not None and hash != downloadHash:
print("Try {}: Downloaded file {} hash {} does not match expected hash {} for url {}".format(i + 1, tempFileName, downloadHash, hash, url))
os.remove(tempFileName)
continue
return tempFileName
raise RuntimeError("Downloaded file hash {} does not match expected hash {} for\n{}".format(downloadHash, hash, url))
def downloadAndExtract(url, destPath, hash=None, hasher=hashlib.sha512(), isZip=False):
tempFileName = downloadFile(url, hash, hasher)
if isZip:
with zipfile.ZipFile(tempFileName) as zip:
zip.extractall(destPath)
else:
# Extract the archive
with tarfile.open(tempFileName, 'r:gz') as tgz:
tgz.extractall(destPath)
os.remove(tempFileName)

216
hifi_vcpkg.py Normal file
View file

@ -0,0 +1,216 @@
import hifi_utils
import hifi_android
import hashlib
import os
import platform
import re
import shutil
import tempfile
import json
import xml.etree.ElementTree as ET
import functools
print = functools.partial(print, flush=True)
# Encapsulates the vcpkg system
class VcpkgRepo:
CMAKE_TEMPLATE = """
set(CMAKE_TOOLCHAIN_FILE "{}" CACHE FILEPATH "Toolchain file")
set(CMAKE_TOOLCHAIN_FILE_UNCACHED "{}")
set(VCPKG_INSTALL_ROOT "{}")
set(VCPKG_TOOLS_DIR "{}")
"""
CMAKE_TEMPLATE_NON_ANDROID = """
# If the cached cmake toolchain path is different from the computed one, exit
if(NOT (CMAKE_TOOLCHAIN_FILE_UNCACHED STREQUAL CMAKE_TOOLCHAIN_FILE))
message(FATAL_ERROR "CMAKE_TOOLCHAIN_FILE has changed, please wipe the build directory and rerun cmake")
endif()
"""
def __init__(self, args):
self.args = args
# our custom ports, relative to the script location
self.sourcePortsPath = args.ports_path
self.id = hifi_utils.hashFolder(self.sourcePortsPath)[:8]
self.configFilePath = os.path.join(args.build_root, 'vcpkg.cmake')
# OS dependent information
system = platform.system()
if self.args.vcpkg_root is not None:
self.path = args.vcpkg_root
else:
if 'Darwin' == system:
defaultBasePath = os.path.expanduser('~/hifi/vcpkg')
else:
defaultBasePath = os.path.join(tempfile.gettempdir(), 'hifi', 'vcpkg')
self.basePath = os.getenv('HIFI_VCPKG_BASE', defaultBasePath)
if self.basePath == defaultBasePath:
print("Warning: Environment variable HIFI_VCPKG_BASE not set, using {}".format(defaultBasePath))
if self.args.android:
self.basePath = os.path.join(self.basePath, 'android')
if (not os.path.isdir(self.basePath)):
os.makedirs(self.basePath)
self.path = os.path.join(self.basePath, self.id)
print("Using vcpkg path {}".format(self.path))
lockDir, lockName = os.path.split(self.path)
lockName += '.lock'
if not os.path.isdir(lockDir):
os.makedirs(lockDir)
self.lockFile = os.path.join(lockDir, lockName)
self.tagFile = os.path.join(self.path, '.id')
# A format version attached to the tag file... increment when you want to force the build systems to rebuild
# without the contents of the ports changing
self.version = 1
self.tagContents = "{}_{}".format(self.id, self.version)
if 'Windows' == system:
self.exe = os.path.join(self.path, 'vcpkg.exe')
self.vcpkgUrl = 'https://hifi-public.s3.amazonaws.com/dependencies/vcpkg/vcpkg-win32.tar.gz?versionId=YZYkDejDRk7L_hrK_WVFthWvisAhbDzZ'
self.vcpkgHash = '3e0ff829a74956491d57666109b3e6b5ce4ed0735c24093884317102387b2cb1b2cd1ff38af9ed9173501f6e32ffa05cc6fe6d470b77a71ca1ffc3e0aa46ab9e'
self.hostTriplet = 'x64-windows'
elif 'Darwin' == system:
self.exe = os.path.join(self.path, 'vcpkg')
self.vcpkgUrl = 'https://hifi-public.s3.amazonaws.com/dependencies/vcpkg/vcpkg-osx.tar.gz?versionId=_fhqSxjfrtDJBvEsQ8L_ODcdUjlpX9cc'
self.vcpkgHash = '519d666d02ef22b87c793f016ca412e70f92e1d55953c8f9bd4ee40f6d9f78c1df01a6ee293907718f3bbf24075cc35492fb216326dfc50712a95858e9cbcb4d'
self.hostTriplet = 'x64-osx'
else:
self.exe = os.path.join(self.path, 'vcpkg')
self.vcpkgUrl = 'https://hifi-public.s3.amazonaws.com/dependencies/vcpkg/vcpkg-linux.tar.gz?versionId=97Nazh24etEVKWz33XwgLY0bvxEfZgMU'
self.vcpkgHash = '6a1ce47ef6621e699a4627e8821ad32528c82fce62a6939d35b205da2d299aaa405b5f392df4a9e5343dd6a296516e341105fbb2dd8b48864781d129d7fba10d'
self.hostTriplet = 'x64-linux'
if self.args.android:
self.triplet = 'arm64-android'
self.androidPackagePath = os.path.join(self.path, 'android')
else:
self.triplet = self.hostTriplet
def upToDate(self):
# Prevent doing a clean if we've explcitly set a directory for vcpkg
if self.args.vcpkg_root is not None:
return True
if self.args.force_build:
print("Force build, out of date")
return False
if not os.path.isfile(self.exe):
print("Exe file {} not found, out of date".format(self.exe))
return False
if not os.path.isfile(self.tagFile):
print("Tag file {} not found, out of date".format(self.tagFile))
return False
with open(self.tagFile, 'r') as f:
storedTag = f.read()
if storedTag != self.tagContents:
print("Tag file {} contents don't match computed tag {}, out of date".format(self.tagFile, self.tagContents))
return False
return True
def clean(self):
print("Cleaning vcpkg installation at {}".format(self.path))
if os.path.isdir(self.path):
print("Removing {}".format(self.path))
shutil.rmtree(self.path, ignore_errors=True)
# Make sure the VCPKG prerequisites are all there.
def bootstrap(self):
if self.upToDate():
return
self.clean()
downloadVcpkg = False
if self.args.force_bootstrap:
print("Forcing bootstrap")
downloadVcpkg = True
if not downloadVcpkg and not os.path.isfile(self.exe):
print("Missing executable, boostrapping")
downloadVcpkg = True
# Make sure we have a vcpkg executable
testFile = os.path.join(self.path, '.vcpkg-root')
if not downloadVcpkg and not os.path.isfile(testFile):
print("Missing {}, bootstrapping".format(testFile))
downloadVcpkg = True
if downloadVcpkg:
print("Fetching vcpkg from {} to {}".format(self.vcpkgUrl, self.path))
hifi_utils.downloadAndExtract(self.vcpkgUrl, self.path, self.vcpkgHash)
print("Replacing port files")
portsPath = os.path.join(self.path, 'ports')
if (os.path.islink(portsPath)):
os.unlink(portsPath)
if (os.path.isdir(portsPath)):
shutil.rmtree(portsPath, ignore_errors=True)
shutil.copytree(self.sourcePortsPath, portsPath)
def run(self, commands):
actualCommands = [self.exe, '--vcpkg-root', self.path]
actualCommands.extend(commands)
print("Running command")
print(actualCommands)
hifi_utils.executeSubprocess(actualCommands, folder=self.path)
def setupDependencies(self):
# Special case for android, grab a bunch of binaries
# FIXME remove special casing for android builds eventually
if self.args.android:
print("Installing Android binaries")
self.setupAndroidDependencies()
print("Installing host tools")
self.run(['install', '--triplet', self.hostTriplet, 'hifi-host-tools'])
# If not android, install the hifi-client-deps libraries
if not self.args.android:
print("Installing build dependencies")
self.run(['install', '--triplet', self.triplet, 'hifi-client-deps'])
def cleanBuilds(self):
# Remove temporary build artifacts
builddir = os.path.join(self.path, 'buildtrees')
if os.path.isdir(builddir):
print("Wiping build trees")
shutil.rmtree(builddir, ignore_errors=True)
def setupAndroidDependencies(self):
# vcpkg prebuilt
if not os.path.isdir(os.path.join(self.path, 'installed', 'arm64-android')):
dest = os.path.join(self.path, 'installed')
url = "https://hifi-public.s3.amazonaws.com/dependencies/vcpkg/vcpkg-arm64-android.tar.gz"
# FIXME I don't know why the hash check frequently fails here. If you examine the file later it has the right hash
#hash = "832f82a4d090046bdec25d313e20f56ead45b54dd06eee3798c5c8cbdd64cce4067692b1c3f26a89afe6ff9917c10e4b601c118bea06d23f8adbfe5c0ec12bc3"
#hifi_utils.downloadAndExtract(url, dest, hash)
hifi_utils.downloadAndExtract(url, dest)
def writeTag(self):
print("Writing tag {} to {}".format(self.tagContents, self.tagFile))
with open(self.tagFile, 'w') as f:
f.write(self.tagContents)
def writeConfig(self):
print("Writing cmake config to {}".format(self.configFilePath))
# Write out the configuration for use by CMake
cmakeScript = os.path.join(self.path, 'scripts/buildsystems/vcpkg.cmake')
installPath = os.path.join(self.path, 'installed', self.triplet)
toolsPath = os.path.join(self.path, 'installed', self.hostTriplet, 'tools')
cmakeTemplate = VcpkgRepo.CMAKE_TEMPLATE
if not self.args.android:
cmakeTemplate += VcpkgRepo.CMAKE_TEMPLATE_NON_ANDROID
cmakeConfig = cmakeTemplate.format(cmakeScript, cmakeScript, installPath, toolsPath).replace('\\', '/')
with open(self.configFilePath, 'w') as f:
f.write(cmakeConfig)
def cleanOldBuilds(self):
# FIXME because we have the base directory, and because a build will
# update the tag file on every run, we can scan the base dir for sub directories containing
# a tag file that is older than N days, and if found, delete the directory, recovering space
print("Not implemented")

View file

@ -16,10 +16,11 @@ import controlsUit 1.0
Preference {
id: root
height: spacer.height + Math.max(hifi.dimensions.controlLineHeight, checkBox.implicitHeight)
property bool value: false
Component.onCompleted: {
checkBox.checked = preference.value;
preference.value = Qt.binding(function(){ return checkBox.checked; });
value = checkBox.checked;
}
function save() {
@ -47,6 +48,7 @@ Preference {
onClicked: {
Tablet.playSound(TabletEnums.ButtonClick);
value = checked;
}
anchors {

View file

@ -100,6 +100,9 @@ Rectangle {
console.log("Failed to get Available Updates", result.data.message);
} else {
exchangeMoneyButtonContainer.messagesWaiting = result.data.updates.length > 0;
if (!exchangeMoneyButtonContainer.messagesWaiting) {
sendToScript({method: 'clearShouldShowDotUpdates'});
}
}
}
}

View file

@ -328,7 +328,7 @@ Rectangle {
HifiStylesUit.RalewayRegular {
text: "Your wallet is not set up.\n" +
"Open the ASSETS app to get started.";
"Open the INVENTORY app to get started.";
// Anchors
anchors.fill: parent;
// Text size

View file

@ -123,12 +123,12 @@ Item {
}
// Runtime customization of preferences.
var locomotionPreference = findPreference("VR Movement", "Teleporting only / Walking and teleporting");
var locomotionPreference = findPreference("VR Movement", "Walking");
var flyingPreference = findPreference("VR Movement", "Jumping and flying");
if (locomotionPreference && flyingPreference) {
flyingPreference.visible = (locomotionPreference.value === 1);
flyingPreference.visible = locomotionPreference.value;
locomotionPreference.valueChanged.connect(function () {
flyingPreference.visible = (locomotionPreference.value === 1);
flyingPreference.visible = locomotionPreference.value;
});
}
if (HMD.isHeadControllerAvailable("Oculus")) {

View file

@ -100,6 +100,7 @@
#include <MainWindow.h>
#include <MappingRequest.h>
#include <MessagesClient.h>
#include <hfm/ModelFormatRegistry.h>
#include <model-networking/ModelCacheScriptingInterface.h>
#include <model-networking/TextureCacheScriptingInterface.h>
#include <ModelEntityItem.h>
@ -837,6 +838,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) {
DependencyManager::set<NodeList>(NodeType::Agent, listenPort);
DependencyManager::set<recording::ClipCache>();
DependencyManager::set<GeometryCache>();
DependencyManager::set<ModelFormatRegistry>(); // ModelFormatRegistry must be defined before ModelCache. See the ModelCache constructor.
DependencyManager::set<ModelCache>();
DependencyManager::set<ModelCacheScriptingInterface>();
DependencyManager::set<ScriptCache>();
@ -2666,6 +2668,7 @@ Application::~Application() {
DependencyManager::destroy<TextureCache>();
DependencyManager::destroy<ModelCacheScriptingInterface>();
DependencyManager::destroy<ModelCache>();
DependencyManager::destroy<ModelFormatRegistry>();
DependencyManager::destroy<ScriptCache>();
DependencyManager::destroy<SoundCacheScriptingInterface>();
DependencyManager::destroy<SoundCache>();

View file

@ -247,6 +247,7 @@ class MyAvatar : public Avatar {
Q_PROPERTY(bool isInSittingState READ getIsInSittingState WRITE setIsInSittingState);
Q_PROPERTY(MyAvatar::SitStandModelType userRecenterModel READ getUserRecenterModel WRITE setUserRecenterModel);
Q_PROPERTY(bool isSitStandStateLocked READ getIsSitStandStateLocked WRITE setIsSitStandStateLocked);
Q_PROPERTY(bool allowTeleporting READ getAllowTeleporting)
const QString DOMINANT_LEFT_HAND = "left";
const QString DOMINANT_RIGHT_HAND = "right";
@ -559,6 +560,9 @@ public:
void setUseAdvancedMovementControls(bool useAdvancedMovementControls)
{ _useAdvancedMovementControls.set(useAdvancedMovementControls); }
bool getAllowTeleporting() { return _allowTeleportingSetting.get(); }
void setAllowTeleporting(bool allowTeleporting) { _allowTeleportingSetting.set(allowTeleporting); }
bool getShowPlayArea() const { return _showPlayArea.get(); }
void setShowPlayArea(bool showPlayArea) { _showPlayArea.set(showPlayArea); }
@ -1889,6 +1893,7 @@ private:
Setting::Handle<float> _userHeightSetting;
Setting::Handle<bool> _flyingHMDSetting;
Setting::Handle<int> _avatarEntityCountSetting;
Setting::Handle<bool> _allowTeleportingSetting { "allowTeleporting", true };
std::vector<Setting::Handle<QUuid>> _avatarEntityIDSettings;
std::vector<Setting::Handle<QByteArray>> _avatarEntityDataSettings;
};

View file

@ -56,9 +56,11 @@ void GraphicsEngine::initializeGPU(GLWidget* glwidget) {
glwidget->makeCurrent();
_gpuContext = std::make_shared<gpu::Context>();
#ifndef Q_OS_ANDROID
_gpuContext->pushProgramsToSync(shader::allPrograms(), [this] {
_programsCompiled.store(true);
}, 1);
#endif
DependencyManager::get<TextureCache>()->setGPUContext(_gpuContext);
}

View file

@ -86,7 +86,11 @@ protected:
FrameTimingsScriptingInterface _frameTimingsScriptingInterface;
std::shared_ptr<ProceduralSkybox> _splashScreen { std::make_shared<ProceduralSkybox>() };
#ifndef Q_OS_ANDROID
std::atomic<bool> _programsCompiled { false };
#else
std::atomic<bool> _programsCompiled { true };
#endif
friend class Application;
};

View file

@ -10,6 +10,7 @@
#ifndef hifi_RenderEventHandler_h
#define hifi_RenderEventHandler_h
#include <functional>
#include <QEvent>
#include <QElapsedTimer>
#include "gl/OffscreenGLCanvas.h"
@ -49,4 +50,4 @@ private:
bool event(QEvent* event) override;
};
#endif // #include hifi_RenderEventHandler_h
#endif // #include hifi_RenderEventHandler_h

View file

@ -238,14 +238,15 @@ void setupPreferences() {
static const QString VR_MOVEMENT{ "VR Movement" };
{
auto getter = [myAvatar]()->int { return myAvatar->useAdvancedMovementControls() ? 1 : 0; };
auto setter = [myAvatar](int value) { myAvatar->setUseAdvancedMovementControls(value == 1); };
auto preference =
new RadioButtonsPreference(VR_MOVEMENT, "Teleporting only / Walking and teleporting", getter, setter);
QStringList items;
items << "Teleporting only" << "Walking and teleporting";
preference->setHeading("Movement mode");
preference->setItems(items);
auto getter = [myAvatar]()->bool { return myAvatar->getAllowTeleporting(); };
auto setter = [myAvatar](bool value) { myAvatar->setAllowTeleporting(value); };
auto preference = new CheckPreference(VR_MOVEMENT, "Teleporting", getter, setter);
preferences->addPreference(preference);
}
{
auto getter = [myAvatar]()->bool { return myAvatar->useAdvancedMovementControls(); };
auto setter = [myAvatar](bool value) { myAvatar->setUseAdvancedMovementControls(value); };
auto preference = new CheckPreference(VR_MOVEMENT, "Walking", getter, setter);
preferences->addPreference(preference);
}
{

View file

@ -20,6 +20,7 @@
#include <Profile.h>
#include "AnimationLogging.h"
#include <FBXSerializer.h>
int animationPointerMetaTypeId = qRegisterMetaType<AnimationPointer>();

View file

@ -17,7 +17,7 @@
#include <QtScript/QScriptValue>
#include <DependencyManager.h>
#include <FBXSerializer.h>
#include <hfm/HFM.h>
#include <ResourceCache.h>
class Animation;

View file

@ -773,6 +773,14 @@ bool RenderableModelEntityItem::shouldBePhysical() const {
}
}
int RenderableModelEntityItem::getJointParent(int index) const {
auto model = getModel();
if (model) {
return model->getRig().getJointParentIndex(index);
}
return -1;
}
glm::quat RenderableModelEntityItem::getAbsoluteJointRotationInObjectFrame(int index) const {
auto model = getModel();
if (model) {

View file

@ -94,6 +94,7 @@ public:
// these are in the frame of this object (model space)
virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override;
virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override;
virtual int getJointParent(int index) const override;
virtual bool setAbsoluteJointRotationInObjectFrame(int index, const glm::quat& rotation) override;
virtual bool setAbsoluteJointTranslationInObjectFrame(int index, const glm::vec3& translation) override;

View file

@ -467,6 +467,7 @@ public:
// these are in the frame of this object
virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override { return glm::quat(); }
virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override { return glm::vec3(0.0f); }
virtual int getJointParent(int index) const override { return -1; }
virtual bool setLocalJointRotation(int index, const glm::quat& rotation) override { return false; }
virtual bool setLocalJointTranslation(int index, const glm::vec3& translation) override { return false; }

View file

@ -1834,6 +1834,15 @@ glm::vec3 EntityScriptingInterface::localCoordsToVoxelCoords(const QUuid& entity
}
}
int EntityScriptingInterface::getJointParent(const QUuid& entityID, int index) {
if (auto entity = checkForTreeEntityAndTypeMatch(entityID, EntityTypes::Model)) {
auto modelEntity = std::dynamic_pointer_cast<ModelEntityItem>(entity);
return modelEntity->getJointParent(index);
} else {
return -1;
}
}
glm::vec3 EntityScriptingInterface::getAbsoluteJointTranslationInObjectFrame(const QUuid& entityID, int jointIndex) {
if (auto entity = checkForTreeEntityAndTypeMatch(entityID, EntityTypes::Model)) {
auto modelEntity = std::dynamic_pointer_cast<ModelEntityItem>(entity);

View file

@ -1007,7 +1007,16 @@ public slots:
*/
// FIXME move to a renderable entity interface
Q_INVOKABLE glm::vec3 getAbsoluteJointTranslationInObjectFrame(const QUuid& entityID, int jointIndex);
/**jsdoc
* Get the index of the parent joint.
* @function Entities.getJointParent
* @param {Uuid} entityID - The ID of the entity.
* @param {number} index - The integer index of the joint.
* @returns {number} The index of the parent joint.
*/
Q_INVOKABLE int getJointParent(const QUuid& entityID, int index);
/**jsdoc
* Get the translation of a joint in a {@link Entities.EntityType|Model} entity relative to the entity's position and
* orientation.

View file

@ -1854,6 +1854,17 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
return hfmModelPtr;
}
MediaType FBXSerializer::getMediaType() const {
MediaType mediaType("fbx");
mediaType.extensions.push_back("fbx");
mediaType.fileSignatures.emplace_back("Kaydara FBX Binary \x00", 0);
return mediaType;
}
std::unique_ptr<hfm::Serializer::Factory> FBXSerializer::getFactory() const {
return std::make_unique<hfm::Serializer::SimpleFactory<FBXSerializer>>();
}
HFMModel::Pointer FBXSerializer::read(const QByteArray& data, const QVariantHash& mapping, const QUrl& url) {
QBuffer buffer(const_cast<QByteArray*>(&data));
buffer.open(QIODevice::ReadOnly);

View file

@ -96,6 +96,9 @@ class ExtractedMesh;
class FBXSerializer : public HFMSerializer {
public:
MediaType getMediaType() const override;
std::unique_ptr<hfm::Serializer::Factory> getFactory() const override;
HFMModel* _hfmModel;
/// Reads HFMModel from the supplied model and mapping data.
/// \exception QString if an error occurs in parsing

View file

@ -35,11 +35,6 @@
#include "FBXSerializer.h"
GLTFSerializer::GLTFSerializer() {
}
bool GLTFSerializer::getStringVal(const QJsonObject& object, const QString& fieldname,
QString& value, QMap<QString, bool>& defined) {
bool _defined = (object.contains(fieldname) && object[fieldname].isString());
@ -910,6 +905,17 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const QUrl& url) {
return true;
}
MediaType GLTFSerializer::getMediaType() const {
MediaType mediaType("gltf");
mediaType.extensions.push_back("gltf");
mediaType.webMediaTypes.push_back("model/gltf+json");
return mediaType;
}
std::unique_ptr<hfm::Serializer::Factory> GLTFSerializer::getFactory() const {
return std::make_unique<hfm::Serializer::SimpleFactory<GLTFSerializer>>();
}
HFMModel::Pointer GLTFSerializer::read(const QByteArray& data, const QVariantHash& mapping, const QUrl& url) {
_url = url;

View file

@ -16,7 +16,6 @@
#include <QtNetwork/QNetworkReply>
#include <hfm/ModelFormatLogging.h>
#include <hfm/HFMSerializer.h>
#include "FBXSerializer.h"
struct GLTFAsset {
@ -703,7 +702,9 @@ struct GLTFFile {
class GLTFSerializer : public QObject, public HFMSerializer {
Q_OBJECT
public:
GLTFSerializer();
MediaType getMediaType() const override;
std::unique_ptr<hfm::Serializer::Factory> getFactory() const override;
HFMModel::Pointer read(const QByteArray& data, const QVariantHash& mapping, const QUrl& url = QUrl()) override;
private:
GLTFFile _file;

View file

@ -651,6 +651,15 @@ done:
return result;
}
MediaType OBJSerializer::getMediaType() const {
MediaType mediaType("obj");
mediaType.extensions.push_back("obj");
return mediaType;
}
std::unique_ptr<hfm::Serializer::Factory> OBJSerializer::getFactory() const {
return std::make_unique<hfm::Serializer::SimpleFactory<OBJSerializer>>();
}
HFMModel::Pointer OBJSerializer::read(const QByteArray& data, const QVariantHash& mapping, const QUrl& url) {
PROFILE_RANGE_EX(resource_parse, __FUNCTION__, 0xffff0000, nullptr);

View file

@ -14,7 +14,6 @@
#include <QtNetwork/QNetworkReply>
#include <hfm/HFMSerializer.h>
#include "FBXSerializer.h"
class OBJTokenizer {
public:
@ -92,6 +91,9 @@ public:
class OBJSerializer: public QObject, public HFMSerializer { // QObject so we can make network requests.
Q_OBJECT
public:
MediaType getMediaType() const override;
std::unique_ptr<hfm::Serializer::Factory> getFactory() const override;
typedef QVector<OBJFace> FaceGroup;
QVector<glm::vec3> vertices;
QVector<glm::vec3> vertexColors;

View file

@ -0,0 +1,65 @@
//
// HFMFormatRegistry.cpp
// libraries/hfm/src/hfm
//
// Created by Sabrina Shanman on 2018/11/29.
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "HFMFormatRegistry.h"
namespace hfm {
FormatRegistry::MediaTypeID FormatRegistry::registerMediaType(const MediaType& mediaType, std::unique_ptr<Serializer::Factory> supportedFactory) {
std::lock_guard<std::mutex> lock(_libraryLock);
MediaTypeID id = _mediaTypeLibrary.registerMediaType(mediaType);
_supportedFormats.emplace_back(id, supportedFactory);
return id;
}
void FormatRegistry::unregisterMediaType(const MediaTypeID& mediaTypeID) {
std::lock_guard<std::mutex> lock(_libraryLock);
for (auto it = _supportedFormats.begin(); it != _supportedFormats.end(); it++) {
if ((*it).mediaTypeID == mediaTypeID) {
_supportedFormats.erase(it);
break;
}
}
_mediaTypeLibrary.unregisterMediaType(mediaTypeID);
}
std::shared_ptr<Serializer> FormatRegistry::getSerializerForMediaTypeID(FormatRegistry::MediaTypeID mediaTypeID) const {
// TODO: shared_lock in C++14
std::lock_guard<std::mutex> lock(*const_cast<std::mutex*>(&_libraryLock));
for (auto it = _supportedFormats.begin(); it != _supportedFormats.end(); it++) {
if ((*it).mediaTypeID == mediaTypeID) {
return (*it).factory->get();
}
}
return std::shared_ptr<Serializer>();
}
std::shared_ptr<Serializer> FormatRegistry::getSerializerForMediaType(const hifi::ByteArray& data, const hifi::URL& url, const std::string& webMediaType) const {
MediaTypeID id;
{
// TODO: shared_lock in C++14
std::lock_guard<std::mutex> lock(*const_cast<std::mutex*>(&_libraryLock));
id = _mediaTypeLibrary.findMediaTypeForData(data);
if (id == INVALID_MEDIA_TYPE_ID) {
id = _mediaTypeLibrary.findMediaTypeForURL(url);
if (id == INVALID_MEDIA_TYPE_ID) {
id = _mediaTypeLibrary.findMediaTypeForWebID(webMediaType);
}
}
}
return getSerializerForMediaTypeID(id);
}
};

View file

@ -0,0 +1,50 @@
//
// HFMFormatRegistry.h
// libraries/hfm/src/hfm
//
// Created by Sabrina Shanman on 2018/11/28.
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_HFMFormatRegistry_h
#define hifi_HFMFormatRegistry_h
#include "HFMSerializer.h"
#include <shared/MediaTypeLibrary.h>
#include <shared/ReadWriteLockable.h>
namespace hfm {
class FormatRegistry {
public:
using MediaTypeID = MediaTypeLibrary::ID;
static const MediaTypeID INVALID_MEDIA_TYPE_ID { MediaTypeLibrary::INVALID_ID };
MediaTypeID registerMediaType(const MediaType& mediaType, std::unique_ptr<Serializer::Factory> supportedFactory);
void unregisterMediaType(const MediaTypeID& id);
std::shared_ptr<Serializer> getSerializerForMediaType(const hifi::ByteArray& data, const hifi::URL& url, const std::string& webMediaType) const;
protected:
std::shared_ptr<Serializer> getSerializerForMediaTypeID(MediaTypeID id) const;
MediaTypeLibrary _mediaTypeLibrary;
std::mutex _libraryLock;
class SupportedFormat {
public:
SupportedFormat(const MediaTypeID& mediaTypeID, std::unique_ptr<Serializer::Factory>& factory) :
mediaTypeID(mediaTypeID),
factory(std::move(factory)) {
}
MediaTypeID mediaTypeID;
std::unique_ptr<Serializer::Factory> factory;
};
std::vector<SupportedFormat> _supportedFormats;
};
};
#endif // hifi_HFMFormatRegistry_h

View file

@ -15,10 +15,27 @@
#include <shared/HifiTypes.h>
#include "HFM.h"
#include <shared/MediaTypeLibrary.h>
namespace hfm {
class Serializer {
public:
class Factory {
public:
virtual std::shared_ptr<Serializer> get() = 0;
};
template<typename T>
class SimpleFactory : public Factory {
std::shared_ptr<Serializer> get() override {
return std::make_shared<T>();
}
};
virtual MediaType getMediaType() const = 0;
virtual std::unique_ptr<Factory> getFactory() const = 0;
virtual Model::Pointer read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url = hifi::URL()) = 0;
};

View file

@ -0,0 +1,20 @@
//
// ModelFormatRegistry.cpp
// libraries/model-networking/src/model-networking
//
// Created by Sabrina Shanman on 2018/11/30.
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "ModelFormatRegistry.h"
void ModelFormatRegistry::addFormat(const hfm::Serializer& serializer) {
_hfmFormatRegistry.registerMediaType(serializer.getMediaType(), serializer.getFactory());
}
std::shared_ptr<hfm::Serializer> ModelFormatRegistry::getSerializerForMediaType(const hifi::ByteArray& data, const hifi::URL& url, const std::string& webMediaType) const {
return _hfmFormatRegistry.getSerializerForMediaType(data, url, webMediaType);
}

View file

@ -0,0 +1,28 @@
//
// ModelFormatRegistry.h
// libraries/hfm/src/hfm
//
// Created by Sabrina Shanman on 2018/11/30.
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_ModelFormatRegistry_h
#define hifi_ModelFormatRegistry_h
#include "HFMFormatRegistry.h"
#include <DependencyManager.h>
class ModelFormatRegistry : public Dependency {
public:
void addFormat(const hfm::Serializer& serializer);
std::shared_ptr<hfm::Serializer> getSerializerForMediaType(const hifi::ByteArray& data, const hifi::URL& url, const std::string& webMediaType) const;
protected:
hfm::FormatRegistry _hfmFormatRegistry;
};
#endif // hifi_ModelFormatRegistry_h

View file

@ -12,9 +12,6 @@
#include "ModelCache.h"
#include <Finally.h>
#include <FSTReader.h>
#include "FBXSerializer.h"
#include "OBJSerializer.h"
#include "GLTFSerializer.h"
#include <gpu/Batch.h>
#include <gpu/Stream.h>
@ -26,6 +23,10 @@
#include "ModelNetworkingLogging.h"
#include <Trace.h>
#include <StatTracker.h>
#include <hfm/ModelFormatRegistry.h>
#include <FBXSerializer.h>
#include <OBJSerializer.h>
#include <GLTFSerializer.h>
Q_LOGGING_CATEGORY(trace_resource_parse_geometry, "trace.resource.parse.geometry")
@ -144,9 +145,9 @@ void GeometryMappingResource::onGeometryMappingLoaded(bool success) {
class GeometryReader : public QRunnable {
public:
GeometryReader(QWeakPointer<Resource>& resource, const QUrl& url, const QVariantHash& mapping,
const QByteArray& data, bool combineParts) :
_resource(resource), _url(url), _mapping(mapping), _data(data), _combineParts(combineParts) {
GeometryReader(const ModelLoader& modelLoader, QWeakPointer<Resource>& resource, const QUrl& url, const QVariantHash& mapping,
const QByteArray& data, bool combineParts, const QString& webMediaType) :
_modelLoader(modelLoader), _resource(resource), _url(url), _mapping(mapping), _data(data), _combineParts(combineParts), _webMediaType(webMediaType) {
DependencyManager::get<StatTracker>()->incrementStat("PendingProcessing");
}
@ -154,11 +155,13 @@ public:
virtual void run() override;
private:
ModelLoader _modelLoader;
QWeakPointer<Resource> _resource;
QUrl _url;
QVariantHash _mapping;
QByteArray _data;
bool _combineParts;
QString _webMediaType;
};
void GeometryReader::run() {
@ -183,62 +186,53 @@ void GeometryReader::run() {
throw QString("reply is NULL");
}
QString urlname = _url.path().toLower();
if (!urlname.isEmpty() && !_url.path().isEmpty() &&
// Ensure the resource has not been deleted
auto resource = _resource.toStrongRef();
if (!resource) {
qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref";
return;
}
(_url.path().toLower().endsWith(".fbx") ||
_url.path().toLower().endsWith(".obj") ||
_url.path().toLower().endsWith(".obj.gz") ||
_url.path().toLower().endsWith(".gltf"))) {
HFMModel::Pointer hfmModel;
QVariantHash serializerMapping = _mapping;
serializerMapping["combineParts"] = _combineParts;
if (_url.path().toLower().endsWith(".fbx")) {
hfmModel = FBXSerializer().read(_data, serializerMapping, _url);
if (hfmModel->meshes.size() == 0 && hfmModel->joints.size() == 0) {
throw QString("empty geometry, possibly due to an unsupported FBX version");
}
} else if (_url.path().toLower().endsWith(".obj")) {
hfmModel = OBJSerializer().read(_data, serializerMapping, _url);
} else if (_url.path().toLower().endsWith(".obj.gz")) {
QByteArray uncompressedData;
if (gunzip(_data, uncompressedData)){
hfmModel = OBJSerializer().read(uncompressedData, serializerMapping, _url);
} else {
throw QString("failed to decompress .obj.gz");
}
} else if (_url.path().toLower().endsWith(".gltf")) {
hfmModel = GLTFSerializer().read(_data, serializerMapping, _url);
if (hfmModel->meshes.size() == 0 && hfmModel->joints.size() == 0) {
throw QString("empty geometry, possibly due to an unsupported GLTF version");
}
} else {
throw QString("unsupported format");
}
// Add scripts to hfmModel
if (!_mapping.value(SCRIPT_FIELD).isNull()) {
QVariantList scripts = _mapping.values(SCRIPT_FIELD);
for (auto &script : scripts) {
hfmModel->scripts.push_back(script.toString());
}
}
// Ensure the resource has not been deleted
auto resource = _resource.toStrongRef();
if (!resource) {
qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref";
} else {
QMetaObject::invokeMethod(resource.data(), "setGeometryDefinition",
Q_ARG(HFMModel::Pointer, hfmModel));
}
} else {
if (_url.path().isEmpty()) {
throw QString("url is invalid");
}
HFMModel::Pointer hfmModel;
QVariantHash serializerMapping = _mapping;
serializerMapping["combineParts"] = _combineParts;
if (_url.path().toLower().endsWith(".gz")) {
QByteArray uncompressedData;
if (!gunzip(_data, uncompressedData)) {
throw QString("failed to decompress .gz model");
}
// Strip the compression extension from the path, so the loader can infer the file type from what remains.
// This is okay because we don't expect the serializer to be able to read the contents of a compressed model file.
auto strippedUrl = _url;
strippedUrl.setPath(_url.path().left(_url.path().size() - 3));
hfmModel = _modelLoader.load(uncompressedData, serializerMapping, strippedUrl, "");
} else {
hfmModel = _modelLoader.load(_data, serializerMapping, _url, _webMediaType.toStdString());
}
if (!hfmModel) {
throw QString("unsupported format");
}
if (hfmModel->meshes.empty() || hfmModel->joints.empty()) {
throw QString("empty geometry, possibly due to an unsupported model version");
}
// Add scripts to hfmModel
if (!_mapping.value(SCRIPT_FIELD).isNull()) {
QVariantList scripts = _mapping.values(SCRIPT_FIELD);
for (auto &script : scripts) {
hfmModel->scripts.push_back(script.toString());
}
}
QMetaObject::invokeMethod(resource.data(), "setGeometryDefinition",
Q_ARG(HFMModel::Pointer, hfmModel));
} catch (const std::exception&) {
auto resource = _resource.toStrongRef();
if (resource) {
@ -258,8 +252,8 @@ void GeometryReader::run() {
class GeometryDefinitionResource : public GeometryResource {
Q_OBJECT
public:
GeometryDefinitionResource(const QUrl& url, const QVariantHash& mapping, const QUrl& textureBaseUrl, bool combineParts) :
GeometryResource(url, resolveTextureBaseUrl(url, textureBaseUrl)), _mapping(mapping), _combineParts(combineParts) {}
GeometryDefinitionResource(const ModelLoader& modelLoader, const QUrl& url, const QVariantHash& mapping, const QUrl& textureBaseUrl, bool combineParts) :
GeometryResource(url, resolveTextureBaseUrl(url, textureBaseUrl)), _modelLoader(modelLoader), _mapping(mapping), _combineParts(combineParts) {}
QString getType() const override { return "GeometryDefinition"; }
@ -269,6 +263,7 @@ protected:
Q_INVOKABLE void setGeometryDefinition(HFMModel::Pointer hfmModel);
private:
ModelLoader _modelLoader;
QVariantHash _mapping;
bool _combineParts;
};
@ -278,7 +273,7 @@ void GeometryDefinitionResource::downloadFinished(const QByteArray& data) {
_url = _effectiveBaseURL;
_textureBaseUrl = _effectiveBaseURL;
}
QThreadPool::globalInstance()->start(new GeometryReader(_self, _effectiveBaseURL, _mapping, data, _combineParts));
QThreadPool::globalInstance()->start(new GeometryReader(_modelLoader, _self, _effectiveBaseURL, _mapping, data, _combineParts, _request->getWebMediaType()));
}
void GeometryDefinitionResource::setGeometryDefinition(HFMModel::Pointer hfmModel) {
@ -316,6 +311,11 @@ ModelCache::ModelCache() {
const qint64 GEOMETRY_DEFAULT_UNUSED_MAX_SIZE = DEFAULT_UNUSED_MAX_SIZE;
setUnusedResourceCacheSize(GEOMETRY_DEFAULT_UNUSED_MAX_SIZE);
setObjectName("ModelCache");
auto modelFormatRegistry = DependencyManager::get<ModelFormatRegistry>();
modelFormatRegistry->addFormat(FBXSerializer());
modelFormatRegistry->addFormat(OBJSerializer());
modelFormatRegistry->addFormat(GLTFSerializer());
}
QSharedPointer<Resource> ModelCache::createResource(const QUrl& url, const QSharedPointer<Resource>& fallback,
@ -328,7 +328,7 @@ QSharedPointer<Resource> ModelCache::createResource(const QUrl& url, const QShar
auto mapping = geometryExtra ? geometryExtra->mapping : QVariantHash();
auto textureBaseUrl = geometryExtra ? geometryExtra->textureBaseUrl : QUrl();
bool combineParts = geometryExtra ? geometryExtra->combineParts : true;
resource = new GeometryDefinitionResource(url, mapping, textureBaseUrl, combineParts);
resource = new GeometryDefinitionResource(_modelLoader, url, mapping, textureBaseUrl, combineParts);
}
return QSharedPointer<Resource>(resource, &Resource::deleter);

View file

@ -20,6 +20,7 @@
#include "FBXSerializer.h"
#include "TextureCache.h"
#include "ModelLoader.h"
// Alias instead of derive to avoid copying
@ -158,6 +159,7 @@ protected:
private:
ModelCache();
virtual ~ModelCache() = default;
ModelLoader _modelLoader;
};
class NetworkMaterial : public graphics::Material {

View file

@ -0,0 +1,24 @@
//
// ModelLoader.cpp
// libraries/model-networking/src/model-networking
//
// Created by Sabrina Shanman on 2018/11/14.
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "ModelLoader.h"
#include <DependencyManager.h>
#include <hfm/ModelFormatRegistry.h>
hfm::Model::Pointer ModelLoader::load(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url, const std::string& webMediaType) const {
auto serializer = DependencyManager::get<ModelFormatRegistry>()->getSerializerForMediaType(data, url, webMediaType);
if (!serializer) {
return hfm::Model::Pointer();
}
return serializer->read(data, mapping, url);
}

View file

@ -0,0 +1,26 @@
//
// ModelLoader.h
// libraries/model-networking/src/model-networking
//
// Created by Sabrina Shanman on 2018/11/13.
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_ModelLoader_h
#define hifi_ModelLoader_h
#include <shared/HifiTypes.h>
#include <hfm/HFM.h>
class ModelLoader {
public:
// Given the currently stored list of supported file formats, determine how to load a model from the given parameters.
// If successful, return an owned reference to the newly loaded model.
// If failed, return an empty reference.
hfm::Model::Pointer load(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url, const std::string& webMediaType) const;
};
#endif // hifi_ModelLoader_h

View file

@ -94,7 +94,7 @@ void HTTPResourceRequest::onRequestFinished() {
// Content-Range: <unit> <range-start>-<range-end>/*
// Content-Range: <unit> */<size>
//
auto parseContentRangeHeader = [](QString contentRangeHeader) -> std::pair<bool, uint64_t> {
static auto parseContentRangeHeader = [](QString contentRangeHeader) -> std::pair<bool, uint64_t> {
auto unitRangeParts = contentRangeHeader.split(' ');
if (unitRangeParts.size() != 2) {
return { false, 0 };
@ -115,6 +115,15 @@ void HTTPResourceRequest::onRequestFinished() {
}
};
static auto parseMediaType = [](QString contentTypeHeader) -> std::pair<bool, QString> {
auto contentTypeParts = contentTypeHeader.split(';');
if (contentTypeParts.size() < 1) {
return { false, "" };
}
return { true, contentTypeParts[0] };
};
switch(_reply->error()) {
case QNetworkReply::NoError:
_data = _reply->readAll();
@ -141,6 +150,16 @@ void HTTPResourceRequest::onRequestFinished() {
}
}
{
auto contentTypeHeader = _reply->rawHeader("Content-Type");
bool success;
QString mediaType;
std::tie(success, mediaType) = parseMediaType(contentTypeHeader);
if (success) {
_webMediaType = mediaType;
}
}
recordBytesDownloadedInStats(STAT_HTTP_RESOURCE_TOTAL_BYTES, _data.size());
break;

View file

@ -84,6 +84,7 @@ public:
bool loadedFromCache() const { return _loadedFromCache; }
bool getRangeRequestSuccessful() const { return _rangeRequestSuccessful; }
bool getTotalSizeOfResource() const { return _totalSizeOfResource; }
QString getWebMediaType() const { return _webMediaType; }
void setFailOnRedirect(bool failOnRedirect) { _failOnRedirect = failOnRedirect; }
void setCacheEnabled(bool value) { _cacheEnabled = value; }
@ -111,6 +112,7 @@ protected:
ByteRange _byteRange;
bool _rangeRequestSuccessful { false };
uint64_t _totalSizeOfResource { 0 };
QString _webMediaType;
int64_t _lastRecordedBytesDownloaded { 0 };
bool _isObservable;
qint64 _callerId;

View file

@ -748,6 +748,18 @@ const Transform SpatiallyNestable::getTransform() const {
return result;
}
void SpatiallyNestable::breakParentingLoop() const {
// someone created a loop. break it...
qCDebug(shared) << "Parenting loop detected: " << getID();
SpatiallyNestablePointer _this = getThisPointer();
_this->setParentID(QUuid());
bool setPositionSuccess;
AACube aaCube = getQueryAACube(setPositionSuccess);
if (setPositionSuccess) {
_this->setWorldPosition(aaCube.calcCenter());
}
}
const Transform SpatiallyNestable::getTransform(int jointIndex, bool& success, int depth) const {
// this returns the world-space transform for this object. It finds its parent's transform (which may
// cause this object's parent to query its parent, etc) and multiplies this object's local transform onto it.
@ -755,15 +767,7 @@ const Transform SpatiallyNestable::getTransform(int jointIndex, bool& success, i
if (depth > MAX_PARENTING_CHAIN_SIZE) {
success = false;
// someone created a loop. break it...
qCDebug(shared) << "Parenting loop detected: " << getID();
SpatiallyNestablePointer _this = getThisPointer();
_this->setParentID(QUuid());
bool setPositionSuccess;
AACube aaCube = getQueryAACube(setPositionSuccess);
if (setPositionSuccess) {
_this->setWorldPosition(aaCube.calcCenter());
}
breakParentingLoop();
return jointInWorldFrame;
}
@ -1208,8 +1212,12 @@ AACube SpatiallyNestable::getQueryAACube() const {
return result;
}
bool SpatiallyNestable::hasAncestorOfType(NestableType nestableType) const {
bool success;
bool SpatiallyNestable::hasAncestorOfType(NestableType nestableType, int depth) const {
if (depth > MAX_PARENTING_CHAIN_SIZE) {
breakParentingLoop();
return false;
}
if (nestableType == NestableType::Avatar) {
QUuid parentID = getParentID();
if (parentID == AVATAR_SELF_ID) {
@ -1217,6 +1225,7 @@ bool SpatiallyNestable::hasAncestorOfType(NestableType nestableType) const {
}
}
bool success;
SpatiallyNestablePointer parent = getParentPointer(success);
if (!success || !parent) {
return false;
@ -1226,11 +1235,14 @@ bool SpatiallyNestable::hasAncestorOfType(NestableType nestableType) const {
return true;
}
return parent->hasAncestorOfType(nestableType);
return parent->hasAncestorOfType(nestableType, depth + 1);
}
const QUuid SpatiallyNestable::findAncestorOfType(NestableType nestableType) const {
bool success;
const QUuid SpatiallyNestable::findAncestorOfType(NestableType nestableType, int depth) const {
if (depth > MAX_PARENTING_CHAIN_SIZE) {
breakParentingLoop();
return QUuid();
}
if (nestableType == NestableType::Avatar) {
QUuid parentID = getParentID();
@ -1239,6 +1251,7 @@ const QUuid SpatiallyNestable::findAncestorOfType(NestableType nestableType) con
}
}
bool success;
SpatiallyNestablePointer parent = getParentPointer(success);
if (!success || !parent) {
return QUuid();
@ -1248,7 +1261,7 @@ const QUuid SpatiallyNestable::findAncestorOfType(NestableType nestableType) con
return parent->getID();
}
return parent->findAncestorOfType(nestableType);
return parent->findAncestorOfType(nestableType, depth + 1);
}
void SpatiallyNestable::getLocalTransformAndVelocities(
@ -1336,7 +1349,12 @@ void SpatiallyNestable::dump(const QString& prefix) const {
}
}
bool SpatiallyNestable::isParentPathComplete() const {
bool SpatiallyNestable::isParentPathComplete(int depth) const {
if (depth > MAX_PARENTING_CHAIN_SIZE) {
breakParentingLoop();
return false;
}
static const QUuid IDENTITY;
QUuid parentID = getParentID();
if (parentID.isNull() || parentID == IDENTITY) {
@ -1349,5 +1367,5 @@ bool SpatiallyNestable::isParentPathComplete() const {
return false;
}
return parent->isParentPathComplete();
return parent->isParentPathComplete(depth + 1);
}

View file

@ -75,7 +75,7 @@ public:
static QString nestableTypeToString(NestableType nestableType);
virtual bool isParentPathComplete() const;
virtual bool isParentPathComplete(int depth = 0) const;
// world frame
@ -163,6 +163,8 @@ public:
virtual glm::vec3 getAbsoluteJointScaleInObjectFrame(int index) const { return glm::vec3(1.0f); }
virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const { return glm::quat(); }
virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const { return glm::vec3(); }
virtual int getJointParent(int index) const { return -1; }
virtual bool setAbsoluteJointRotationInObjectFrame(int index, const glm::quat& rotation) { return false; }
virtual bool setAbsoluteJointTranslationInObjectFrame(int index, const glm::vec3& translation) {return false; }
@ -187,8 +189,8 @@ public:
bool isParentIDValid() const { bool success = false; getParentPointer(success); return success; }
virtual SpatialParentTree* getParentTree() const { return nullptr; }
bool hasAncestorOfType(NestableType nestableType) const;
const QUuid findAncestorOfType(NestableType nestableType) const;
bool hasAncestorOfType(NestableType nestableType, int depth = 0) const;
const QUuid findAncestorOfType(NestableType nestableType, int depth = 0) const;
SpatiallyNestablePointer getParentPointer(bool& success) const;
static SpatiallyNestablePointer findByID(QUuid id, bool& success);
@ -246,6 +248,8 @@ private:
mutable bool _parentKnowsMe { false };
bool _isDead { false };
bool _queryAACubeIsPuffed { false };
void breakParentingLoop() const;
};

View file

@ -0,0 +1,85 @@
//
// MediaTypeLibrary.cpp
// libraries/shared/src/shared
//
// Created by Sabrina Shanman on 2018/11/29.
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "MediaTypeLibrary.h"
MediaType MediaType::NONE = MediaType("");
MediaTypeLibrary::ID MediaTypeLibrary::registerMediaType(const MediaType& mediaType) {
ID id = nextID++;
_mediaTypes.emplace_back(id, mediaType);
return id;
}
void MediaTypeLibrary::unregisterMediaType(const MediaTypeLibrary::ID& id) {
for (auto it = _mediaTypes.begin(); it != _mediaTypes.end(); it++) {
if ((*it).id == id) {
_mediaTypes.erase(it);
break;
}
}
}
MediaType MediaTypeLibrary::getMediaType(const MediaTypeLibrary::ID& id) const {
for (auto& supportedFormat : _mediaTypes) {
if (supportedFormat.id == id) {
return supportedFormat.mediaType;
}
}
return MediaType::NONE;
}
MediaTypeLibrary::ID MediaTypeLibrary::findMediaTypeForData(const hifi::ByteArray& data) const {
// Check file contents
for (auto& mediaType : _mediaTypes) {
for (auto& fileSignature : mediaType.mediaType.fileSignatures) {
auto testBytes = data.mid(fileSignature.byteOffset, (int)fileSignature.bytes.size()).toStdString();
if (testBytes == fileSignature.bytes) {
return mediaType.id;
}
}
}
return INVALID_ID;
}
MediaTypeLibrary::ID MediaTypeLibrary::findMediaTypeForURL(const hifi::URL& url) const {
// Check file extension
std::string urlString = url.path().toStdString();
std::size_t extensionSeparator = urlString.rfind('.');
if (extensionSeparator != std::string::npos) {
std::string detectedExtension = urlString.substr(extensionSeparator + 1);
for (auto& supportedFormat : _mediaTypes) {
for (auto& extension : supportedFormat.mediaType.extensions) {
if (extension == detectedExtension) {
return supportedFormat.id;
}
}
}
}
return INVALID_ID;
}
MediaTypeLibrary::ID MediaTypeLibrary::findMediaTypeForWebID(const std::string& webMediaType) const {
// Check web media type
if (webMediaType != "") {
for (auto& supportedFormat : _mediaTypes) {
for (auto& candidateWebMediaType : supportedFormat.mediaType.webMediaTypes) {
if (candidateWebMediaType == webMediaType) {
return supportedFormat.id;
}
}
}
}
return INVALID_ID;
}

View file

@ -0,0 +1,90 @@
//
// MediaTypeLibrary.h
// libraries/shared/src/shared
//
// Created by Sabrina Shanman on 2018/11/28.
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_MediaTypeLibrary_h
#define hifi_MediaTypeLibrary_h
#include <vector>
#include <string>
#include <functional>
#include <mutex>
#include "HifiTypes.h"
// A short sequence of bytes, typically at the beginning of the file, which identifies the file format
class FileSignature {
public:
FileSignature(const std::string& bytes, int byteOffset) :
bytes(bytes),
byteOffset(byteOffset) {
}
FileSignature(const FileSignature& fileSignature) :
bytes(fileSignature.bytes),
byteOffset(fileSignature.byteOffset) {
}
std::string bytes;
int byteOffset;
};
// A named file extension with a list of known ways to positively identify the file type
class MediaType {
public:
MediaType(const std::string& name) :
name(name) {
}
MediaType() {};
MediaType(const MediaType& mediaType) :
name(mediaType.name),
extensions(mediaType.extensions),
webMediaTypes(mediaType.webMediaTypes),
fileSignatures(mediaType.fileSignatures) {
}
static MediaType NONE;
std::string name;
std::vector<std::string> extensions;
std::vector<std::string> webMediaTypes;
std::vector<FileSignature> fileSignatures;
};
class MediaTypeLibrary {
public:
using ID = unsigned int;
static const ID INVALID_ID { 0 };
ID registerMediaType(const MediaType& mediaType);
void unregisterMediaType(const ID& id);
MediaType getMediaType(const ID& id) const;
ID findMediaTypeForData(const hifi::ByteArray& data) const;
ID findMediaTypeForURL(const hifi::URL& url) const;
ID findMediaTypeForWebID(const std::string& webMediaType) const;
protected:
ID nextID { 1 };
class Entry {
public:
Entry(const ID& id, const MediaType& mediaType) :
id(id),
mediaType(mediaType) {
}
ID id;
MediaType mediaType;
};
std::vector<Entry> _mediaTypes;
};
#endif // hifi_MeidaTypeLibrary_h

View file

@ -1,5 +1,26 @@
#!python
# The prebuild script is intended to simplify life for developers and dev-ops. It's repsonsible for acquiring
# tools required by the build as well as dependencies on which we rely.
#
# By using this script, we can reduce the requirements for a developer getting started to:
#
# * A working C++ dev environment like visual studio, xcode, gcc, or clang
# * Qt
# * CMake
# * Python 3.x
#
# The function of the build script is to acquire, if not already present, all the other build requirements
# The build script should be idempotent. If you run it with the same arguments multiple times, that should
# have no negative impact on the subsequent build times (i.e. re-running the prebuild script should not
# trigger a header change that causes files to be rebuilt). Subsequent runs after the first run should
# execute quickly, determining that no work is to be done
import hifi_singleton
import hifi_utils
import hifi_android
import hifi_vcpkg
import argparse
import concurrent
import hashlib
@ -9,252 +30,65 @@ import os
import platform
import shutil
import ssl
import subprocess
import sys
import tarfile
import re
import tempfile
import time
import urllib.request
import functools
print = functools.partial(print, flush=True)
def executeSubprocess(processArgs, folder=None, env=None):
restoreDir = None
if folder != None:
restoreDir = os.getcwd()
os.chdir(folder)
process = subprocess.Popen(
processArgs, stdout=sys.stdout, stderr=sys.stderr, env=env)
process.wait()
if (0 != process.returncode):
raise RuntimeError('Call to "{}" failed.\n\narguments:\n{}\n'.format(
processArgs[0],
' '.join(processArgs[1:]),
))
if restoreDir != None:
os.chdir(restoreDir)
def hashFile(file):
hasher = hashlib.sha512()
with open(file, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hasher.update(chunk)
return hasher.hexdigest()
def hashFolder(folder):
hasher = hashlib.sha256()
for dirName, subdirList, fileList in os.walk(folder):
for fname in fileList:
with open(os.path.join(folder, dirName, fname), "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hasher.update(chunk)
return hasher.hexdigest()
def downloadAndExtract(url, destPath, hash=None):
tempFileDescriptor, tempFileName = tempfile.mkstemp()
# OSX Python doesn't support SSL, so we need to bypass it.
# However, we still validate the downloaded file's sha512 hash
context = ssl._create_unverified_context()
with urllib.request.urlopen(url, context=context) as response, open(tempFileDescriptor, 'wb') as tempFile:
shutil.copyfileobj(response, tempFile)
# Verify the hash
if hash and hash != hashFile(tempFileName):
raise RuntimeError("Downloaded file does not match hash")
# Extract the archive
with tarfile.open(tempFileName, 'r:gz') as tgz:
tgz.extractall(destPath)
os.remove(tempFileName)
class VcpkgRepo:
def __init__(self):
global args
scriptPath = os.path.dirname(os.path.realpath(sys.argv[0]))
# our custom ports, relative to the script location
self.sourcePortsPath = os.path.join(scriptPath, 'cmake', 'ports')
# FIXME Revert to ports hash before release
self.id = hashFolder(self.sourcePortsPath)[:8]
# OS dependent information
system = platform.system()
if args.vcpkg_root is not None:
print("override vcpkg path with " + args.vcpkg_root)
self.path = args.vcpkg_root
else:
if 'Darwin' == system:
defaultBasePath = os.path.expanduser('~/hifi/vcpkg')
else:
defaultBasePath = os.path.join(tempfile.gettempdir(), 'hifi', 'vcpkg')
basePath = os.getenv('HIFI_VCPKG_BASE', defaultBasePath)
if (not os.path.isdir(basePath)):
os.makedirs(basePath)
self.path = os.path.join(basePath, self.id)
self.tagFile = os.path.join(self.path, '.id')
# A format version attached to the tag file... increment when you want to force the build systems to rebuild
# without the contents of the ports changing
self.version = 1
self.tagContents = "{}_{}".format(self.id, self.version)
print("prebuild path: " + self.path)
if 'Windows' == system:
self.exe = os.path.join(self.path, 'vcpkg.exe')
self.vcpkgUrl = 'https://hifi-public.s3.amazonaws.com/dependencies/vcpkg/vcpkg-win32.tar.gz?versionId=YZYkDejDRk7L_hrK_WVFthWvisAhbDzZ'
self.vcpkgHash = '3e0ff829a74956491d57666109b3e6b5ce4ed0735c24093884317102387b2cb1b2cd1ff38af9ed9173501f6e32ffa05cc6fe6d470b77a71ca1ffc3e0aa46ab9e'
self.hostTriplet = 'x64-windows'
elif 'Darwin' == system:
self.exe = os.path.join(self.path, 'vcpkg')
self.vcpkgUrl = 'https://hifi-public.s3.amazonaws.com/dependencies/vcpkg/vcpkg-osx.tar.gz?versionId=_fhqSxjfrtDJBvEsQ8L_ODcdUjlpX9cc'
self.vcpkgHash = '519d666d02ef22b87c793f016ca412e70f92e1d55953c8f9bd4ee40f6d9f78c1df01a6ee293907718f3bbf24075cc35492fb216326dfc50712a95858e9cbcb4d'
self.hostTriplet = 'x64-osx'
else:
self.exe = os.path.join(self.path, 'vcpkg')
self.vcpkgUrl = 'https://hifi-public.s3.amazonaws.com/dependencies/vcpkg/vcpkg-linux.tar.gz?versionId=97Nazh24etEVKWz33XwgLY0bvxEfZgMU'
self.vcpkgHash = '6a1ce47ef6621e699a4627e8821ad32528c82fce62a6939d35b205da2d299aaa405b5f392df4a9e5343dd6a296516e341105fbb2dd8b48864781d129d7fba10d'
self.hostTriplet = 'x64-linux'
if args.android:
self.triplet = 'arm64-android'
else:
self.triplet = self.hostTriplet
def outOfDate(self):
global args
# Prevent doing a clean if we've explcitly set a directory for vcpkg
if args.vcpkg_root is not None:
return False
if args.force_build:
return True
print("Looking for tag file {}".format(self.tagFile))
if not os.path.isfile(self.tagFile):
return True
with open(self.tagFile, 'r') as f:
storedTag = f.read()
print("Found stored tag {}".format(storedTag))
if storedTag != self.tagContents:
print("Doesn't match computed tag {}".format(self.tagContents))
return True
return False
def clean(self):
cleanPath = self.path
print("Cleaning vcpkg installation at {}".format(cleanPath))
if os.path.isdir(self.path):
print("Removing {}".format(cleanPath))
shutil.rmtree(cleanPath, ignore_errors=True)
def bootstrap(self):
global args
if self.outOfDate():
self.clean()
# don't download the vcpkg binaries if we're working with an explicit
# vcpkg directory (possibly a git checkout)
if args.vcpkg_root is None:
downloadVcpkg = False
if args.force_bootstrap:
print("Forcing bootstrap")
downloadVcpkg = True
if not downloadVcpkg and not os.path.isfile(self.exe):
print("Missing executable, boostrapping")
downloadVcpkg = True
# Make sure we have a vcpkg executable
testFile = os.path.join(self.path, '.vcpkg-root')
if not downloadVcpkg and not os.path.isfile(testFile):
print("Missing {}, bootstrapping".format(testFile))
downloadVcpkg = True
if downloadVcpkg:
print("Fetching vcpkg from {} to {}".format(self.vcpkgUrl, self.path))
downloadAndExtract(self.vcpkgUrl, self.path, self.vcpkgHash)
print("Replacing port files")
portsPath = os.path.join(self.path, 'ports')
if (os.path.islink(portsPath)):
os.unlink(portsPath)
if (os.path.isdir(portsPath)):
shutil.rmtree(portsPath, ignore_errors=True)
shutil.copytree(self.sourcePortsPath, portsPath)
def downloadAndroidDependencies(self):
url = "https://hifi-public.s3.amazonaws.com/dependencies/vcpkg/vcpkg-arm64-android.tar.gz"
hash = "832f82a4d090046bdec25d313e20f56ead45b54dd06eee3798c5c8cbdd64cce4067692b1c3f26a89afe6ff9917c10e4b601c118bea06d23f8adbfe5c0ec12bc3"
dest = os.path.join(self.path, 'installed')
downloadAndExtract(url, dest, hash)
def run(self, commands):
actualCommands = [self.exe, '--vcpkg-root', self.path]
actualCommands.extend(commands)
print("Running command")
print(actualCommands)
executeSubprocess(actualCommands, folder=self.path)
def buildDependencies(self):
global args
print("Installing host tools")
self.run(['install', '--triplet', self.hostTriplet, 'hifi-host-tools'])
# Special case for android, grab a bunch of binaries
if args.android:
self.downloadAndroidDependencies()
return
print("Installing build dependencies")
self.run(['install', '--triplet', self.triplet, 'hifi-client-deps'])
# Remove temporary build artifacts
builddir = os.path.join(self.path, 'buildtrees')
if os.path.isdir(builddir):
print("Wiping build trees")
shutil.rmtree(builddir, ignore_errors=True)
def writeConfig(self):
global args
configFilePath = os.path.join(args.build_root, 'vcpkg.cmake')
print("Writing cmake config to {}".format(configFilePath))
# Write out the configuration for use by CMake
cmakeScript = os.path.join(self.path, 'scripts/buildsystems/vcpkg.cmake')
installPath = os.path.join(self.path, 'installed', self.triplet)
toolsPath = os.path.join(self.path, 'installed', self.hostTriplet, 'tools')
cmakeTemplate = 'set(CMAKE_TOOLCHAIN_FILE "{}" CACHE FILEPATH "Toolchain file")\n'
cmakeTemplate += 'set(VCPKG_INSTALL_ROOT "{}" CACHE FILEPATH "vcpkg installed packages path")\n'
cmakeTemplate += 'set(VCPKG_TOOLS_DIR "{}" CACHE FILEPATH "vcpkg installed packages path")\n'
cmakeConfig = cmakeTemplate.format(cmakeScript, installPath, toolsPath).replace('\\', '/')
with open(configFilePath, 'w') as f:
f.write(cmakeConfig)
def writeTag(self):
print("Writing tag {} to {}".format(self.tagContents, self.tagFile))
with open(self.tagFile, 'w') as f:
f.write(self.tagContents)
def parse_args():
# our custom ports, relative to the script location
defaultPortsPath = hifi_utils.scriptRelative('cmake', 'ports')
from argparse import ArgumentParser
parser = ArgumentParser(description='Prepare build dependencies.')
parser.add_argument('--android', action='store_true')
#parser.add_argument('--android', type=str)
parser.add_argument('--debug', action='store_true')
parser.add_argument('--force-bootstrap', action='store_true')
parser.add_argument('--force-build', action='store_true')
parser.add_argument('--vcpkg-root', type=str, help='The location of the vcpkg distribution')
parser.add_argument('--build-root', required=True, type=str, help='The location of the cmake build')
parser.add_argument('--ports-path', type=str, default=defaultPortsPath)
if True:
args = parser.parse_args()
else:
args = parser.parse_args(['--android', 'questInterface', '--build-root', 'C:/git/hifi/android/apps/questInterface/.externalNativeBuild/cmake/debug/arm64-v8a'])
return args
def main():
vcpkg = VcpkgRepo()
vcpkg.bootstrap()
vcpkg.buildDependencies()
vcpkg.writeConfig()
vcpkg.writeTag()
# Fixup env variables. Leaving `USE_CCACHE` on will cause scribe to fail to build
# VCPKG_ROOT seems to cause confusion on Windows systems that previously used it for
# building OpenSSL
removeEnvVars = ['VCPKG_ROOT', 'USE_CCACHE']
for var in removeEnvVars:
if var in os.environ:
del os.environ[var]
args = parse_args()
# Only allow one instance of the program to run at a time
pm = hifi_vcpkg.VcpkgRepo(args)
with hifi_singleton.Singleton(pm.lockFile) as lock:
if not pm.upToDate():
pm.bootstrap()
# Always write the tag, even if we changed nothing. This
# allows vcpkg to reclaim disk space by identifying directories with
# tags that haven't been touched in a long time
pm.writeTag()
from argparse import ArgumentParser
parser = ArgumentParser(description='Prepare build dependencies.')
parser.add_argument('--android', action='store_true')
parser.add_argument('--debug', action='store_true')
parser.add_argument('--force-bootstrap', action='store_true')
parser.add_argument('--force-build', action='store_true')
parser.add_argument('--vcpkg-root', type=str, help='The location of the vcpkg distribution')
parser.add_argument('--build-root', required=True, type=str, help='The location of the cmake build')
# Grab our required dependencies:
# * build host tools, like spirv-cross and scribe
# * build client dependencies like openssl and nvtt
pm.setupDependencies()
args = parser.parse_args()
# wipe out the build directories (after writing the tag, since failure
# here shouldn't invalidte the vcpkg install)
pm.cleanBuilds()
# Write the vcpkg config to the build directory last
pm.writeConfig()
print(sys.argv)
main()

View file

@ -536,6 +536,10 @@ function fromQml(message) {
shouldShowDotHistory = false;
ui.messagesWaiting(shouldShowDotUpdates || shouldShowDotHistory);
break;
case 'clearShouldShowDotUpdates':
shouldShowDotUpdates = false;
ui.messagesWaiting(shouldShowDotUpdates || shouldShowDotHistory);
break;
case 'http.request':
// Handled elsewhere, don't log.
break;
@ -578,7 +582,7 @@ function notificationDataProcessPageHistory(data) {
var shouldShowDotUpdates = false;
function notificationPollCallbackUpdates(updatesArray) {
shouldShowDotUpdates = shouldShowDotUpdates || updatesArray.length > 0;
shouldShowDotUpdates = updatesArray.length > 0;
ui.messagesWaiting(shouldShowDotUpdates || shouldShowDotHistory);
if (updatesArray.length > 0) {

View file

@ -701,7 +701,7 @@ Script.include("/~/system/libraries/controllers.js");
};
this.isReady = function(controllerData, deltaTime) {
if (Window.interstitialModeEnabled && !Window.isPhysicsEnabled()) {
if ((Window.interstitialModeEnabled && !Window.isPhysicsEnabled()) || !MyAvatar.allowTeleporting) {
return makeRunningValues(false, [], []);
}

View file

@ -251,19 +251,39 @@
$(this).closest('.col-xs-3').attr("class", 'col-xs-6');
var priceElement = $(this).find('.price');
priceElement.css({
"padding": "3px 5px",
"height": "40px",
"background": "linear-gradient(#00b4ef, #0093C5)",
"color": "#FFF",
"font-weight": "600",
"line-height": "34px"
});
var available = true;
if (priceElement.text() === 'invalidated' ||
priceElement.text() === 'sold out' ||
priceElement.text() === 'not for sale') {
available = false;
priceElement.css({
"padding": "3px 5px 10px 5px",
"height": "40px",
"background": "linear-gradient(#a2a2a2, #fefefe)",
"color": "#000",
"font-weight": "600",
"line-height": "34px"
});
} else {
priceElement.css({
"padding": "3px 5px",
"height": "40px",
"background": "linear-gradient(#00b4ef, #0093C5)",
"color": "#FFF",
"font-weight": "600",
"line-height": "34px"
});
}
if (parseInt(cost) > 0) {
priceElement.css({ "width": "auto" });
priceElement.html('<span class="hifi-glyph hifi-glyph-hfc" style="filter:invert(1);background-size:20px;' +
'width:20px;height:20px;position:relative;top:5px;"></span> ' + cost);
if (available) {
priceElement.html('<span class="hifi-glyph hifi-glyph-hfc" style="filter:invert(1);background-size:20px;' +
'width:20px;height:20px;position:relative;top:5px;"></span> ' + cost);
}
priceElement.css({ "min-width": priceElement.width() + 30 });
}
});

View file

@ -51,6 +51,7 @@
"emitterShouldTrail": 1,
"isEmitting": 1,
"lifespan": 3,
"lifetime": 5,
"maxParticles": 1000,
"particleRadius": 0.003,
"polarStart": Math.PI / 2,
@ -82,6 +83,7 @@
"emitterShouldTrail": 1,
"isEmitting": 1,
"lifespan": 3.6,
"lifetime": 5,
"maxParticles": 4000,
"particleRadius": 0.048,
"polarStart": 0,
@ -287,6 +289,11 @@
}
function updateMakingConnection() {
if (!makingConnectionParticleEffect) {
particleEffectUpdateTimer = null;
return;
}
makingConnectionEmitRate = Math.max(makingConnectionEmitRate * MAKING_CONNECTION_DECAY_RATE,
MAKING_CONNECTION_MINIMUM_EMIT_RATE);
isMakingConnectionEmitting = true;
@ -302,6 +309,11 @@
}
function updateParticleEffect() {
if (!particleEffect) {
particleEffectUpdateTimer = null;
return;
}
particleEmitRate = Math.max(PARTICLE_MINIMUM_EMIT_RATE, particleEmitRate * PARTICLE_DECAY_RATE);
Entities.editEntity(particleEffect, {
emitRate: particleEmitRate

View file

@ -5,7 +5,7 @@ Nitpick is a stand alone application that provides a mechanism for regression te
* The snapshots are compared to a 'canonical' set of images that have been produced beforehand.
* The result, if any test failed, is a zipped folder describing the failure.
Nitpick has 5 functions, separated into 4 tabs:
Nitpick has 5 functions, separated into separate tabs:
1. Creating tests, MD files and recursive scripts
1. Windows task bar utility (Windows only)
1. Running tests
@ -22,9 +22,9 @@ Nitpick is built as part of the High Fidelity build.
1. Select all, right-click and select 7-Zip->Add to archive...
1. Set Archive format to 7z
1. Check "Create SFX archive
1. Enter installer name (i.e. `nitpick-installer-v1.1.exe`)
1. Enter installer name (i.e. `nitpick-installer-v1.2.exe`)
1. Click "OK"
1. Copy created installer to https://hifi-qa.s3.amazonaws.com/nitpick/Windows/nitpick-installer-v1.1.exe: aws s3 cp nitpick-installer-v1.1.exe s3://hifi-qa/nitpick/Mac/nitpick-installer-v1.1.exe
1. Copy created installer to https://hifi-qa.s3.amazonaws.com/nitpick/Windows/nitpick-installer-v1.2.exe: aws s3 cp nitpick-installer-v1.2.exe s3://hifi-qa/nitpick/Mac/nitpick-installer-v1.2.exe
#### Mac
These steps assume the hifi repository has been cloned to `~/hifi`.
1. (first time) Install brew
@ -37,12 +37,12 @@ These steps assume the hifi repository has been cloned to `~/hifi`.
1. Change the loader instruction to find the dynamic library locally
In a terminal: `install_name_tool -change ~/hifi/build/ext/Xcode/quazip/project/lib/libquazip5.1.dylib libquazip5.1.dylib nitpick`
1. Delete any existing disk images. In a terminal: `rm *.dmg`
1. Create installer (note final period).In a terminal: `create-dmg --volname nitpick-installer-v1.1 nitpick-installer-v1.1.dmg .`
1. Create installer (note final period).In a terminal: `create-dmg --volname nitpick-installer-v1.2 nitpick-installer-v1.2.dmg .`
Make sure to wait for completion.
1. Copy created installer to AWS: `~/Library/Python/3.7/bin/aws s3 cp nitpick-installer-v1.1.dmg s3://hifi-qa/nitpick/Mac/nitpick-installer-v1.1.dmg`
1. Copy created installer to AWS: `~/Library/Python/3.7/bin/aws s3 cp nitpick-installer-v1.2.dmg s3://hifi-qa/nitpick/Mac/nitpick-installer-v1.2.dmg`
### Installation
#### Windows
1. (First time) download and install vc_redist.x64.exe (available at https://hifi-qa.s3.amazonaws.com/nitpick/Windows/nitpick-installer-v1.1.exe)
1. (First time) download and install vc_redist.x64.exe (available at https://hifi-qa.s3.amazonaws.com/nitpick/Windows/nitpick-installer-v1.2.exe)
1. (First time) download and install Python 3 from https://hifi-qa.s3.amazonaws.com/nitpick/Windows/python-3.7.0-amd64.exe (also located at https://www.python.org/downloads/)
1. After installation - create an environment variable called PYTHON_PATH and set it to the folder containing the Python executable.
1. (First time) download and install AWS CLI from https://hifi-qa.s3.amazonaws.com/nitpick/Windows/AWSCLI64PY3.msi (also available at https://aws.amazon.com/cli/
@ -52,7 +52,7 @@ These steps assume the hifi repository has been cloned to `~/hifi`.
1. Leave region name and ouput format as default [None]
1. Install the latest release of Boto3 via pip: `pip install boto3`
1. Download the installer by browsing to [here](<https://hifi-qa.s3.amazonaws.com/nitpick/Windows/nitpick-installer-v1.1.exe>)
1. Download the installer by browsing to [here](<https://hifi-qa.s3.amazonaws.com/nitpick/Windows/nitpick-installer-v1.2.exe>)
1. Double click on the installer and install to a convenient location
![](./setup_7z.PNG)
@ -76,14 +76,14 @@ In a terminal: `python3 get-pip.py --user`
1. Enter the secret key
1. Leave region name and ouput format as default [None]
1. Install the latest release of Boto3 via pip: pip3 install boto3
1. Download the installer by browsing to [here](<https://hifi-qa.s3.amazonaws.com/nitpick/Mac/nitpick-installer-v1.1.dmg>).
1. Download the installer by browsing to [here](<https://hifi-qa.s3.amazonaws.com/nitpick/Mac/nitpick-installer-v1.2.dmg>).
1. Double-click on the downloaded image to mount it
1. Create a folder for the nitpick files (e.g. ~/nitpick)
If this folder exists then delete all it's contents.
1. Copy the downloaded files to the folder
In a terminal:
`cd ~/nitpick`
`cp -r /Volumes/nitpick-installer-v1.1/* .`
`cp -r /Volumes/nitpick-installer-v1.2/* .`
1. __To run nitpick, cd to the folder that you copied to and run `./nitpick`__
# Usage

View file

@ -10,6 +10,8 @@
#include "AWSInterface.h"
#include <QDirIterator>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMessageBox>
#include <QProcess>
@ -22,12 +24,12 @@ AWSInterface::AWSInterface(QObject* parent) : QObject(parent) {
}
void AWSInterface::createWebPageFromResults(const QString& testResults,
const QString& snapshotDirectory,
const QString& workingDirectory,
QCheckBox* updateAWSCheckBox,
QLineEdit* urlLineEdit) {
_testResults = testResults;
_snapshotDirectory = snapshotDirectory;
_workingDirectory = workingDirectory;
_urlLineEdit = urlLineEdit;
_urlLineEdit->setEnabled(false);
@ -36,6 +38,9 @@ void AWSInterface::createWebPageFromResults(const QString& testResults,
if (updateAWSCheckBox->isChecked()) {
updateAWS();
QMessageBox::information(0, "Success", "HTML file has been created and copied to AWS");
} else {
QMessageBox::information(0, "Success", "HTML file has been created");
}
}
@ -43,14 +48,14 @@ void AWSInterface::extractTestFailuresFromZippedFolder() {
// For a test results zip file called `D:/tt/TestResults--2018-10-02_16-54-11(9426)[DESKTOP-PMKNLSQ].zip`
// the folder will be called `TestResults--2018-10-02_16-54-11(9426)[DESKTOP-PMKNLSQ]`
// and, this folder will be in the working directory
QStringList parts =_testResults.split('/');
QString zipFolderName = _snapshotDirectory + "/" + parts[parts.length() - 1].split('.')[0];
QStringList parts = _testResults.split('/');
QString zipFolderName = _workingDirectory + "/" + parts[parts.length() - 1].split('.')[0];
if (QDir(zipFolderName).exists()) {
QDir dir = zipFolderName;
dir.removeRecursively();
}
JlCompress::extractDir(_testResults, _snapshotDirectory);
JlCompress::extractDir(_testResults, _workingDirectory);
}
void AWSInterface::createHTMLFile() {
@ -60,7 +65,7 @@ void AWSInterface::createHTMLFile() {
QString filename = pathComponents[pathComponents.length() - 1];
_resultsFolder = filename.left(filename.length() - 4);
QString resultsPath = _snapshotDirectory + "/" + _resultsFolder + "/";
QString resultsPath = _workingDirectory + "/" + _resultsFolder + "/";
QDir().mkdir(resultsPath);
_htmlFilename = resultsPath + HTML_FILENAME;
@ -98,65 +103,11 @@ void AWSInterface::writeHead(QTextStream& stream) {
void AWSInterface::writeBody(QTextStream& stream) {
stream << "\t" << "<body>\n";
writeTitle(stream);
writeTable(stream);
stream << "\t" << "</body>\n";
}
void AWSInterface::finishHTMLpage(QTextStream& stream) {
stream << "</html>\n";
}
void AWSInterface::writeTitle(QTextStream& stream) {
// Separate relevant components from the results name
// The expected format is as follows: `D:/tt/snapshots/TestResults--2018-10-04_11-09-41(PR14128)[DESKTOP-PMKNLSQ].zip`
QStringList tokens = _testResults.split('/');
// date_buildorPR_hostName will be 2018-10-03_15-35-28(9433)[DESKTOP-PMKNLSQ]
QString date_buildorPR_hostName = tokens[tokens.length() - 1].split("--")[1].split(".")[0];
QString buildorPR = date_buildorPR_hostName.split('(')[1].split(')')[0];
QString hostName = date_buildorPR_hostName.split('[')[1].split(']')[0];
QStringList dateList = date_buildorPR_hostName.split('(')[0].split('_')[0].split('-');
QString year = dateList[0];
QString month = dateList[1];
QString day = dateList[2];
QStringList timeList = date_buildorPR_hostName.split('(')[0].split('_')[1].split('-');
QString hour = timeList[0];
QString minute = timeList[1];
QString second = timeList[2];
const QString months[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
stream << "\t" << "\t" << "<font color=\"red\">\n";
stream << "\t" << "\t" << "<h1>Failures for ";
stream << months[month.toInt() - 1] << " " << day << ", " << year << ", ";
stream << hour << ":" << minute << ":" << second << ", ";
if (buildorPR.left(2) == "PR") {
stream << "PR " << buildorPR.right(buildorPR.length() - 2) << ", ";
} else {
stream << "build " << buildorPR << ", ";
}
stream << "run on " << hostName << "</h1>\n";
}
void AWSInterface::writeTable(QTextStream& stream) {
QString previousTestName{ "" };
// Loop over all entries in directory. This is done in stages, as the names are not in the order of the tests
// The first stage reads the directory names into a list
// The second stage renames the tests by removing everything up to "--tests."
// The third stage renames the directories
// The fourth and lasts stage creates the HTML entries
//
// Note that failures are processed first, then successes
// The results are read here as they are used both in the title (for the summary) and for table
QStringList originalNamesFailures;
QStringList originalNamesSuccesses;
QDirIterator it1(_snapshotDirectory.toStdString().c_str());
QDirIterator it1(_workingDirectory);
while (it1.hasNext()) {
QString nextDirectory = it1.next();
@ -165,7 +116,7 @@ void AWSInterface::writeTable(QTextStream& stream) {
continue;
}
// Only process failure folders
// Only process result folders
if (!nextDirectory.contains("--tests.")) {
continue;
}
@ -180,6 +131,71 @@ void AWSInterface::writeTable(QTextStream& stream) {
}
}
writeTitle(stream, originalNamesFailures, originalNamesSuccesses);
writeTable(stream, originalNamesFailures, originalNamesSuccesses);
stream << "\t" << "</body>\n";
}
void AWSInterface::finishHTMLpage(QTextStream& stream) {
stream << "</html>\n";
}
void AWSInterface::writeTitle(QTextStream& stream, const QStringList& originalNamesFailures, const QStringList& originalNamesSuccesses) {
// Separate relevant components from the results name
// The expected format is as follows: `D:/tt/snapshots/TestResults--2018-10-04_11-09-41(PR14128)[DESKTOP-PMKNLSQ].zip`
QStringList tokens = _testResults.split('/');
// date_buildorPR_hostName will be 2018-10-03_15-35-28(9433)[DESKTOP-PMKNLSQ]
QString date_buildorPR_hostName = tokens[tokens.length() - 1].split("--")[1].split(".")[0];
QString buildorPR = date_buildorPR_hostName.split('(')[1].split(')')[0];
QString hostName = date_buildorPR_hostName.split('[')[1].split(']')[0];
QStringList dateList = date_buildorPR_hostName.split('(')[0].split('_')[0].split('-');
QString year = dateList[0];
QString month = dateList[1];
QString day = dateList[2];
QStringList timeList = date_buildorPR_hostName.split('(')[0].split('_')[1].split('-');
QString hour = timeList[0];
QString minute = timeList[1];
QString second = timeList[2];
const QString months[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
stream << "\t" << "\t" << "<font color=\"Green\">\n";
stream << "\t" << "\t" << "<h1>Results for ";
stream << months[month.toInt() - 1] << " " << day << ", " << year << ", ";
stream << hour << ":" << minute << ":" << second << ", ";
if (buildorPR.left(2) == "PR") {
stream << "PR " << buildorPR.right(buildorPR.length() - 2) << ", ";
} else {
stream << "build " << buildorPR << ", ";
}
stream << "run on " << hostName << "</h1>\n";
int numberOfFailures = originalNamesFailures.length();
int numberOfSuccesses = originalNamesSuccesses.length();
stream << "<h2>" << QString::number(numberOfFailures) << " failed, out of a total of " << QString::number(numberOfSuccesses) << " tests</h2>\n";
stream << "\t" << "\t" << "<font color=\"red\">\n";
stream << "\t" << "\t" << "<h1>The following tests failed:</h1>";
}
void AWSInterface::writeTable(QTextStream& stream, const QStringList& originalNamesFailures, const QStringList& originalNamesSuccesses) {
QString previousTestName{ "" };
// Loop over all entries in directory. This is done in stages, as the names are not in the order of the tests
// The first stage reads the directory names into a list
// The second stage renames the tests by removing everything up to "--tests."
// The third stage renames the directories
// The fourth and lasts stage creates the HTML entries
//
// Note that failures are processed first, then successes
QStringList newNamesFailures;
for (int i = 0; i < originalNamesFailures.length(); ++i) {
newNamesFailures.append(originalNamesFailures[i].split("--tests.")[1]);
@ -189,11 +205,11 @@ void AWSInterface::writeTable(QTextStream& stream) {
for (int i = 0; i < originalNamesSuccesses.length(); ++i) {
newNamesSuccesses.append(originalNamesSuccesses[i].split("--tests.")[1]);
}
_htmlFailuresFolder = _snapshotDirectory + "/" + _resultsFolder + "/" + FAILURES_FOLDER;
_htmlFailuresFolder = _workingDirectory + "/" + _resultsFolder + "/" + FAILURES_FOLDER;
QDir().mkdir(_htmlFailuresFolder);
_htmlSuccessesFolder = _snapshotDirectory + "/" + _resultsFolder + "/" + SUCCESSES_FOLDER;
_htmlSuccessesFolder = _workingDirectory + "/" + _resultsFolder + "/" + SUCCESSES_FOLDER;
QDir().mkdir(_htmlSuccessesFolder);
for (int i = 0; i < newNamesFailures.length(); ++i) {
@ -204,7 +220,11 @@ void AWSInterface::writeTable(QTextStream& stream) {
QDir().rename(originalNamesSuccesses[i], _htmlSuccessesFolder + "/" + newNamesSuccesses[i]);
}
QDirIterator it2((_htmlFailuresFolder).toStdString().c_str());
// Mac does not read folders in lexicographic order, so this step is divided into 2
// Each test consists of the test name and its index.
QStringList folderNames;
QDirIterator it2(_htmlFailuresFolder);
while (it2.hasNext()) {
QString nextDirectory = it2.next();
@ -214,10 +234,17 @@ void AWSInterface::writeTable(QTextStream& stream) {
}
QStringList pathComponents = nextDirectory.split('/');
QString filename = pathComponents[pathComponents.length() - 1];
int splitIndex = filename.lastIndexOf(".");
QString testName = filename.left(splitIndex).replace(".", " / ");
QString testNumber = filename.right(filename.length() - (splitIndex + 1));
QString folderName = pathComponents[pathComponents.length() - 1];
folderNames << folderName;
}
folderNames.sort();
for (const auto& folderName : folderNames) {
int splitIndex = folderName.lastIndexOf(".");
QString testName = folderName.left(splitIndex).replace('.', " / ");
int testNumber = folderName.right(folderName.length() - (splitIndex + 1)).toInt();
// The failures are ordered lexicographically, so we know that we can rely on the testName changing to create a new table
if (testName != previousTestName) {
@ -228,18 +255,20 @@ void AWSInterface::writeTable(QTextStream& stream) {
previousTestName = testName;
stream << "\t\t<h2>" << testName << "</h2>\n";
openTable(stream);
openTable(stream, folderName, true);
}
createEntry(testNumber.toInt(), filename, stream, true);
createEntry(testNumber, folderName, stream, true);
}
closeTable(stream);
stream << "\t" << "\t" << "<font color=\"blue\">\n";
stream << "\t" << "\t" << "<h1>The following tests passed:</h1>";
QDirIterator it3((_htmlSuccessesFolder).toStdString().c_str());
// Now do the same for passes
folderNames.clear();
QDirIterator it3(_htmlSuccessesFolder);
while (it3.hasNext()) {
QString nextDirectory = it3.next();
@ -249,10 +278,17 @@ void AWSInterface::writeTable(QTextStream& stream) {
}
QStringList pathComponents = nextDirectory.split('/');
QString filename = pathComponents[pathComponents.length() - 1];
int splitIndex = filename.lastIndexOf(".");
QString testName = filename.left(splitIndex).replace(".", " / ");
QString testNumber = filename.right(filename.length() - (splitIndex + 1));
QString folderName = pathComponents[pathComponents.length() - 1];
folderNames << folderName;
}
folderNames.sort();
for (const auto& folderName : folderNames) {
int splitIndex = folderName.lastIndexOf(".");
QString testName = folderName.left(splitIndex).replace('.', " / ");
int testNumber = folderName.right(folderName.length() - (splitIndex + 1)).toInt();
// The failures are ordered lexicographically, so we know that we can rely on the testName changing to create a new table
if (testName != previousTestName) {
@ -263,64 +299,153 @@ void AWSInterface::writeTable(QTextStream& stream) {
previousTestName = testName;
stream << "\t\t<h2>" << testName << "</h2>\n";
openTable(stream);
openTable(stream, folderName, false);
}
createEntry(testNumber.toInt(), filename, stream, false);
createEntry(testNumber, folderName, stream, false);
}
closeTable(stream);
}
void AWSInterface::openTable(QTextStream& stream) {
stream << "\t\t<table>\n";
stream << "\t\t\t<tr>\n";
stream << "\t\t\t\t<th><h1>Test</h1></th>\n";
stream << "\t\t\t\t<th><h1>Actual Image</h1></th>\n";
stream << "\t\t\t\t<th><h1>Expected Image</h1></th>\n";
stream << "\t\t\t\t<th><h1>Difference Image</h1></th>\n";
stream << "\t\t\t</tr>\n";
void AWSInterface::openTable(QTextStream& stream, const QString& testResult, const bool isFailure) {
QStringList resultNameComponents = testResult.split('/');
QString resultName = resultNameComponents[resultNameComponents.length() - 1];
bool textResultsFileFound;
if (isFailure) {
textResultsFileFound = QFile::exists(_htmlFailuresFolder + "/" + resultName + "/Result.txt");
} else {
textResultsFileFound = QFile::exists(_htmlSuccessesFolder + "/" + resultName + "/Result.txt");
}
if (textResultsFileFound) {
if (isFailure) {
stream << "\t\t<table>\n";
stream << "\t\t\t<tr>\n";
stream << "\t\t\t\t<th><h1>Element</h1></th>\n";
stream << "\t\t\t\t<th><h1>Actual Value</h1></th>\n";
stream << "\t\t\t\t<th><h1>Expected Value</h1></th>\n";
stream << "\t\t\t</tr>\n";
} else {
stream << "\t\t<h3>No errors found</h3>\n\n";
stream << "\t\t<h3>===============</h3>\n\n";
}
} else {
stream << "\t\t<table>\n";
stream << "\t\t\t<tr>\n";
stream << "\t\t\t\t<th><h1>Test</h1></th>\n";
stream << "\t\t\t\t<th><h1>Actual Image</h1></th>\n";
stream << "\t\t\t\t<th><h1>Expected Image</h1></th>\n";
stream << "\t\t\t\t<th><h1>Difference Image</h1></th>\n";
stream << "\t\t\t</tr>\n";
}
}
void AWSInterface::closeTable(QTextStream& stream) {
stream << "\t\t</table>\n";
}
void AWSInterface::createEntry(int index, const QString& testResult, QTextStream& stream, const bool isFailure) {
stream << "\t\t\t<tr>\n";
stream << "\t\t\t\t<td><h1>" << QString::number(index) << "</h1></td>\n";
void AWSInterface::createEntry(const int index, const QString& testResult, QTextStream& stream, const bool isFailure) {
// For a test named `D:/t/fgadhcUDHSFaidsfh3478JJJFSDFIUSOEIrf/Failure_1--tests.engine.interaction.pick.collision.many.00000`
// we need `Failure_1--tests.engine.interaction.pick.collision.many.00000`
QStringList resultNameComponents = testResult.split('/');
QString resultName = resultNameComponents[resultNameComponents.length() - 1];
QString textResultFilename;
if (isFailure) {
textResultFilename = _htmlFailuresFolder + "/" + resultName + "/Result.txt";
} else {
textResultFilename = _htmlSuccessesFolder + "/" + resultName + "/Result.txt";
}
bool textResultsFileFound{ QFile::exists(textResultFilename) };
QString folder;
bool differenceFileFound;
if (isFailure) {
folder = FAILURES_FOLDER;
differenceFileFound = QFile::exists(_htmlFailuresFolder + "/" + resultName + "/Difference Image.png");
} else {
folder = SUCCESSES_FOLDER;
folder = SUCCESSES_FOLDER;
differenceFileFound = QFile::exists(_htmlSuccessesFolder + "/" + resultName + "/Difference Image.png");
}
stream << "\t\t\t\t<td><img src=\"./" << folder << "/" << resultName << "/Actual Image.png\" width = \"576\" height = \"324\" ></td>\n";
stream << "\t\t\t\t<td><img src=\"./" << folder << "/" << resultName << "/Expected Image.png\" width = \"576\" height = \"324\" ></td>\n";
if (textResultsFileFound) {
// Parse the JSON file
QFile file;
file.setFileName(textResultFilename);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__),
"Failed to open file " + textResultFilename);
}
if (differenceFileFound) {
stream << "\t\t\t\t<td><img src=\"./" << folder << "/" << resultName << "/Difference Image.png\" width = \"576\" height = \"324\" ></td>\n";
QString value = file.readAll();
file.close();
// The Result.txt file is an object containing elements such as the following:
// "angularDamping": {
// "actual": 0.3938899040222168,
// "expected" : 0.3938899,
// "result" : "pass"
// },
//
// Failures are thos element that have "fail for the result
QJsonDocument document = QJsonDocument::fromJson(value.toUtf8());
QJsonObject json = document.object();
foreach(const QString& key, json.keys()) {
QJsonValue value = json.value(key);
QJsonObject object = value.toObject();
QJsonValue actualValue = object.value("actual");
QString actualValueString;
if (actualValue.isString()) {
actualValueString = actualValue.toString();
} else if (actualValue.isBool()) {
actualValueString = actualValue.toBool() ? "true" : "false";
} else if (actualValue.isDouble()) {
actualValueString = QString::number(actualValue.toDouble());
}
QJsonValue expectedValue = object.value("expected");
QString expectedValueString;
if (expectedValue.isString()) {
expectedValueString = expectedValue.toString();
} else if (expectedValue.isBool()) {
expectedValueString = expectedValue.toBool() ? "true" : "false";
} else if (expectedValue.isDouble()) {
expectedValueString = QString::number(expectedValue.toDouble());
}
QString result = object.value("result").toString();
if (result == "fail") {
stream << "\t\t\t<tr>\n";
stream << "\t\t\t\t<td><font size=\"6\">" + key + "</td>\n";
stream << "\t\t\t\t<td><font size=\"6\">" + actualValueString + "</td>\n";
stream << "\t\t\t\t<td><font size=\"6\">" + expectedValueString + "</td>\n";
stream << "\t\t\t</tr>\n";
}
}
} else {
stream << "\t\t\t\t<td><h2>No Image Found</h2>\n";
stream << "\t\t\t<tr>\n";
stream << "\t\t\t\t<td><h1>" << QString::number(index) << "</h1></td>\n";
stream << "\t\t\t\t<td><img src=\"./" << folder << "/" << resultName << "/Actual Image.png\" width = \"576\" height = \"324\" ></td>\n";
stream << "\t\t\t\t<td><img src=\"./" << folder << "/" << resultName << "/Expected Image.png\" width = \"576\" height = \"324\" ></td>\n";
if (differenceFileFound) {
stream << "\t\t\t\t<td><img src=\"./" << folder << "/" << resultName << "/Difference Image.png\" width = \"576\" height = \"324\" ></td>\n";
} else {
stream << "\t\t\t\t<td><h2>No Image Found</h2>\n";
}
stream << "\t\t\t</tr>\n";
}
stream << "\t\t\t</tr>\n";
}
void AWSInterface::updateAWS() {
QString filename = _snapshotDirectory + "/updateAWS.py";
QString filename = _workingDirectory + "/updateAWS.py";
if (QFile::exists(filename)) {
QFile::remove(filename);
}
@ -337,7 +462,7 @@ void AWSInterface::updateAWS() {
stream << "import boto3\n";
stream << "s3 = boto3.resource('s3')\n\n";
QDirIterator it1(_htmlFailuresFolder.toStdString().c_str());
QDirIterator it1(_htmlFailuresFolder);
while (it1.hasNext()) {
QString nextDirectory = it1.next();
@ -345,26 +470,26 @@ void AWSInterface::updateAWS() {
if (nextDirectory.right(1) == ".") {
continue;
}
// nextDirectory looks like `D:/t/TestResults--2018-10-02_16-54-11(9426)[DESKTOP-PMKNLSQ]/failures/engine.render.effect.bloom.00000`
// We need to concatenate the last 3 components, to get `TestResults--2018-10-02_16-54-11(9426)[DESKTOP-PMKNLSQ]/failures/engine.render.effect.bloom.00000`
QStringList parts = nextDirectory.split('/');
QString filename = parts[parts.length() - 3] + "/" + parts[parts.length() - 2] + "/" + parts[parts.length() - 1];
stream << "data = open('" << _snapshotDirectory << "/" << filename << "/"
stream << "data = open('" << _workingDirectory << "/" << filename << "/"
<< "Actual Image.png"
<< "', 'rb')\n";
stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Actual Image.png" << "', Body=data)\n\n";
stream << "data = open('" << _snapshotDirectory << "/" << filename << "/"
stream << "data = open('" << _workingDirectory << "/" << filename << "/"
<< "Expected Image.png"
<< "', 'rb')\n";
stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Expected Image.png" << "', Body=data)\n\n";
if (QFile::exists(_htmlFailuresFolder + "/" + parts[parts.length() - 1] + "/Difference Image.png")) {
stream << "data = open('" << _snapshotDirectory << "/" << filename << "/"
stream << "data = open('" << _workingDirectory << "/" << filename << "/"
<< "Difference Image.png"
<< "', 'rb')\n";
@ -372,7 +497,7 @@ void AWSInterface::updateAWS() {
}
}
QDirIterator it2(_htmlSuccessesFolder.toStdString().c_str());
QDirIterator it2(_htmlSuccessesFolder);
while (it2.hasNext()) {
QString nextDirectory = it2.next();
@ -386,20 +511,20 @@ void AWSInterface::updateAWS() {
QStringList parts = nextDirectory.split('/');
QString filename = parts[parts.length() - 3] + "/" + parts[parts.length() - 2] + "/" + parts[parts.length() - 1];
stream << "data = open('" << _snapshotDirectory << "/" << filename << "/"
stream << "data = open('" << _workingDirectory << "/" << filename << "/"
<< "Actual Image.png"
<< "', 'rb')\n";
stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Actual Image.png" << "', Body=data)\n\n";
stream << "data = open('" << _snapshotDirectory << "/" << filename << "/"
stream << "data = open('" << _workingDirectory << "/" << filename << "/"
<< "Expected Image.png"
<< "', 'rb')\n";
stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Expected Image.png" << "', Body=data)\n\n";
if (QFile::exists(_htmlSuccessesFolder + "/" + parts[parts.length() - 1] + "/Difference Image.png")) {
stream << "data = open('" << _snapshotDirectory << "/" << filename << "/"
stream << "data = open('" << _workingDirectory << "/" << filename << "/"
<< "Difference Image.png"
<< "', 'rb')\n";
@ -407,7 +532,7 @@ void AWSInterface::updateAWS() {
}
}
stream << "data = open('" << _snapshotDirectory << "/" << _resultsFolder << "/" << HTML_FILENAME << "', 'rb')\n";
stream << "data = open('" << _workingDirectory << "/" << _resultsFolder << "/" << HTML_FILENAME << "', 'rb')\n";
stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << _resultsFolder << "/"
<< HTML_FILENAME << "', Body=data, ContentType='text/html')\n";
@ -426,10 +551,10 @@ void AWSInterface::updateAWS() {
[=](int exitCode, QProcess::ExitStatus exitStatus) { _busyWindow.hide(); });
#ifdef Q_OS_WIN
QStringList parameters = QStringList() << filename ;
QStringList parameters = QStringList() << filename;
process->start(_pythonCommand, parameters);
#elif defined Q_OS_MAC
QStringList parameters = QStringList() << "-c" << _pythonCommand + " " + filename;
QStringList parameters = QStringList() << "-c" << _pythonCommand + " " + filename;
process->start("sh", parameters);
#endif
}
}

View file

@ -26,7 +26,7 @@ public:
explicit AWSInterface(QObject* parent = 0);
void createWebPageFromResults(const QString& testResults,
const QString& snapshotDirectory,
const QString& workingDirectory,
QCheckBox* updateAWSCheckBox,
QLineEdit* urlLineEdit);
@ -38,18 +38,18 @@ public:
void writeBody(QTextStream& stream);
void finishHTMLpage(QTextStream& stream);
void writeTitle(QTextStream& stream);
void writeTable(QTextStream& stream);
void openTable(QTextStream& stream);
void writeTitle(QTextStream& stream, const QStringList& originalNamesFailures, const QStringList& originalNamesSuccesses);
void writeTable(QTextStream& stream, const QStringList& originalNamesFailures, const QStringList& originalNamesSuccesses);
void openTable(QTextStream& stream, const QString& testResult, const bool isFailure);
void closeTable(QTextStream& stream);
void createEntry(int index, const QString& testResult, QTextStream& stream, const bool isFailure);
void createEntry(const int index, const QString& testResult, QTextStream& stream, const bool isFailure);
void updateAWS();
private:
QString _testResults;
QString _snapshotDirectory;
QString _workingDirectory;
QString _resultsFolder;
QString _htmlFailuresFolder;
QString _htmlSuccessesFolder;

View file

@ -105,7 +105,7 @@ int Test::compareImageLists() {
++numberOfFailures;
if (!isInteractiveMode) {
appendTestResultsToFile(_testResultsFolderPath, testResult, _mismatchWindow.getComparisonImage(), true);
appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), true);
} else {
_mismatchWindow.exec();
@ -113,7 +113,7 @@ int Test::compareImageLists() {
case USER_RESPONSE_PASS:
break;
case USE_RESPONSE_FAIL:
appendTestResultsToFile(_testResultsFolderPath, testResult, _mismatchWindow.getComparisonImage(), true);
appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), true);
break;
case USER_RESPONSE_ABORT:
keepOn = false;
@ -124,7 +124,7 @@ int Test::compareImageLists() {
}
}
} else {
appendTestResultsToFile(_testResultsFolderPath, testResult, _mismatchWindow.getComparisonImage(), false);
appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), false);
}
_progressBar->setValue(i);
@ -134,12 +134,36 @@ int Test::compareImageLists() {
return numberOfFailures;
}
void Test::appendTestResultsToFile(const QString& _testResultsFolderPath, TestResult testResult, QPixmap comparisonImage, bool hasFailed) {
int Test::checkTextResults() {
// Create lists of failed and passed tests
QStringList nameFilterFailed;
nameFilterFailed << "*.failed.txt";
QStringList testsFailed = QDir(_snapshotDirectory).entryList(nameFilterFailed, QDir::Files, QDir::Name);
QStringList nameFilterPassed;
nameFilterPassed << "*.passed.txt";
QStringList testsPassed = QDir(_snapshotDirectory).entryList(nameFilterPassed, QDir::Files, QDir::Name);
// Add results to Test Results folder
foreach(QString currentFilename, testsFailed) {
appendTestResultsToFile(currentFilename, true);
}
foreach(QString currentFilename, testsPassed) {
appendTestResultsToFile(currentFilename, false);
}
return testsFailed.length();
}
void Test::appendTestResultsToFile(TestResult testResult, QPixmap comparisonImage, bool hasFailed) {
// Critical error if Test Results folder does not exist
if (!QDir().exists(_testResultsFolderPath)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Folder " + _testResultsFolderPath + " not found");
exit(-1);
}
// There are separate subfolders for failures and passes
QString resultFolderPath;
if (hasFailed) {
resultFolderPath = _testResultsFolderPath + "/Failure_" + QString::number(_failureIndex) + "--" +
@ -195,6 +219,33 @@ void Test::appendTestResultsToFile(const QString& _testResultsFolderPath, TestRe
comparisonImage.save(resultFolderPath + "/" + "Difference Image.png");
}
void::Test::appendTestResultsToFile(QString testResultFilename, bool hasFailed) {
// The test name includes everything until the penultimate period
QString testNameTemp = testResultFilename.left(testResultFilename.lastIndexOf('.'));
QString testName = testResultFilename.left(testNameTemp.lastIndexOf('.'));
QString resultFolderPath;
if (hasFailed) {
resultFolderPath = _testResultsFolderPath + "/Failure_" + QString::number(_failureIndex) + "--" + testName;
++_failureIndex;
} else {
resultFolderPath = _testResultsFolderPath + "/Success_" + QString::number(_successIndex) + "--" + testName;
++_successIndex;
}
if (!QDir().mkdir(resultFolderPath)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__),
"Failed to create folder " + resultFolderPath);
exit(-1);
}
QString source = _snapshotDirectory + "/" + testResultFilename;
QString destination = resultFolderPath + "/Result.txt";
if (!QFile::copy(source, destination)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Failed to copy " + testResultFilename + " to " + resultFolderPath);
exit(-1);
}
}
void Test::startTestsEvaluation(const bool isRunningFromCommandLine,
const bool isRunningInAutomaticTestRun,
const QString& snapshotDirectory,
@ -211,7 +262,7 @@ void Test::startTestsEvaluation(const bool isRunningFromCommandLine,
if (!parent.isNull() && parent.right(1) != "/") {
parent += "/";
}
_snapshotDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select folder containing the test images", parent,
_snapshotDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select folder containing the snapshots", parent,
QFileDialog::ShowDirsOnly);
// If user canceled then restore previous selection and return
@ -270,9 +321,14 @@ void Test::startTestsEvaluation(const bool isRunningFromCommandLine,
nitpick->downloadFiles(expectedImagesURLs, _snapshotDirectory, _expectedImagesFilenames, (void *)this);
}
void Test::finishTestsEvaluation() {
// First - compare the pairs of images
int numberOfFailures = compareImageLists();
// Next - check text results
numberOfFailures += checkTextResults();
if (!_isRunningFromCommandLine && !_isRunningInAutomaticTestRun) {
if (numberOfFailures == 0) {
QMessageBox::information(0, "Success", "All images are as expected");
@ -344,7 +400,7 @@ void Test::createTests() {
parent += "/";
}
_snapshotDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select folder containing the test images", parent,
_snapshotDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select folder containing the snapshots", parent,
QFileDialog::ShowDirsOnly);
// If user canceled then restore previous selection and return
@ -542,7 +598,7 @@ void Test::createAllMDFiles() {
createMDFile(_testsRootDirectory);
}
QDirIterator it(_testsRootDirectory.toStdString().c_str(), QDirIterator::Subdirectories);
QDirIterator it(_testsRootDirectory, QDirIterator::Subdirectories);
while (it.hasNext()) {
QString directory = it.next();
@ -636,7 +692,7 @@ void Test::createAllTestAutoScripts() {
createTestAutoScript(_testsRootDirectory);
}
QDirIterator it(_testsRootDirectory.toStdString().c_str(), QDirIterator::Subdirectories);
QDirIterator it(_testsRootDirectory, QDirIterator::Subdirectories);
while (it.hasNext()) {
QString directory = it.next();
@ -653,7 +709,7 @@ void Test::createAllTestAutoScripts() {
}
}
QMessageBox::information(0, "Success", "'nitpick.js' scripts have been created");
QMessageBox::information(0, "Success", "All 'testAuto.js' scripts have been created");
}
bool Test::createTestAutoScript(const QString& directory) {
@ -704,7 +760,7 @@ void Test::createAllRecursiveScripts() {
createRecursiveScript(_testsRootDirectory, false);
QDirIterator it(_testsRootDirectory.toStdString().c_str(), QDirIterator::Subdirectories);
QDirIterator it(_testsRootDirectory, QDirIterator::Subdirectories);
while (it.hasNext()) {
QString directory = it.next();
@ -716,7 +772,7 @@ void Test::createAllRecursiveScripts() {
// Only process directories that have sub-directories
bool hasNoSubDirectories{ true };
QDirIterator it2(directory.toStdString().c_str(), QDirIterator::Subdirectories);
QDirIterator it2(directory, QDirIterator::Subdirectories);
while (it2.hasNext()) {
QString directory2 = it2.next();
@ -737,16 +793,17 @@ void Test::createAllRecursiveScripts() {
}
void Test::createRecursiveScript(const QString& topLevelDirectory, bool interactiveMode) {
const QString recursiveTestsFilename("testRecursive.js");
QFile allTestsFilename(topLevelDirectory + "/" + recursiveTestsFilename);
if (!allTestsFilename.open(QIODevice::WriteOnly | QIODevice::Text)) {
const QString recursiveTestsScriptName("testRecursive.js");
const QString recursiveTestsFilename(topLevelDirectory + "/" + recursiveTestsScriptName);
QFile recursiveTestsFile(recursiveTestsFilename);
if (!recursiveTestsFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__),
"Failed to create \"" + recursiveTestsFilename + "\" in directory \"" + topLevelDirectory + "\"");
"Failed to create \"" + recursiveTestsScriptName + "\" in directory \"" + topLevelDirectory + "\"");
exit(-1);
}
QTextStream textStream(&allTestsFilename);
QTextStream textStream(&recursiveTestsFile);
textStream << "// This is an automatically generated file, created by nitpick" << endl;
@ -787,7 +844,7 @@ void Test::createRecursiveScript(const QString& topLevelDirectory, bool interact
testFound = true;
}
QDirIterator it(topLevelDirectory.toStdString().c_str(), QDirIterator::Subdirectories);
QDirIterator it(topLevelDirectory, QDirIterator::Subdirectories);
while (it.hasNext()) {
QString directory = it.next();
@ -809,7 +866,15 @@ void Test::createRecursiveScript(const QString& topLevelDirectory, bool interact
if (interactiveMode && !testFound) {
QMessageBox::information(0, "Failure", "No \"" + TEST_FILENAME + "\" files found");
allTestsFilename.close();
recursiveTestsFile.close();
return;
}
// If 'directories' is empty, this means that this recursive script has no tests to call, so it is redundant
// The script will be closed and deleted
if (directories.length() == 0) {
recursiveTestsFile.close();
QFile::remove(recursiveTestsFilename);
return;
}
@ -821,7 +886,7 @@ void Test::createRecursiveScript(const QString& topLevelDirectory, bool interact
textStream << endl;
textStream << "nitpick.runRecursive();" << endl;
allTestsFilename.close();
recursiveTestsFile.close();
}
void Test::createTestsOutline() {
@ -858,7 +923,7 @@ void Test::createTestsOutline() {
int rootDepth { _testDirectory.count('/') };
// Each test is shown as the folder name linking to the matching GitHub URL, and the path to the associated test.md file
QDirIterator it(_testDirectory.toStdString().c_str(), QDirIterator::Subdirectories);
QDirIterator it(_testDirectory, QDirIterator::Subdirectories);
while (it.hasNext()) {
QString directory = it.next();
@ -1052,11 +1117,11 @@ void Test::createWebPage(QCheckBox* updateAWSCheckBox, QLineEdit* urlLineEdit) {
return;
}
QString snapshotDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select a folder to store temporary files in",
QString workingDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select a folder to store temporary files in",
nullptr, QFileDialog::ShowDirsOnly);
if (snapshotDirectory.isNull()) {
if (workingDirectory.isNull()) {
return;
}
_awsInterface.createWebPageFromResults(testResults, snapshotDirectory, updateAWSCheckBox, urlLineEdit);
_awsInterface.createWebPageFromResults(testResults, workingDirectory, updateAWSCheckBox, urlLineEdit);
}

View file

@ -77,6 +77,7 @@ public:
void createRecursiveScript(const QString& topLevelDirectory, bool interactiveMode);
int compareImageLists();
int checkTextResults();
QStringList createListOfAll_imagesInDirectory(const QString& imageFormat, const QString& pathToImageDirectory);
@ -84,7 +85,8 @@ public:
void includeTest(QTextStream& textStream, const QString& testPathname);
void appendTestResultsToFile(const QString& testResultsFolderPath, TestResult testResult, QPixmap comparisonImage, bool hasFailed);
void appendTestResultsToFile(TestResult testResult, QPixmap comparisonImage, bool hasFailed);
void appendTestResultsToFile(QString testResultFilename, bool hasFailed);
bool createTestResultsFolderPath(const QString& directory);
QString zipAndDeleteTestResultsFolder();

View file

@ -275,7 +275,7 @@ void TestRailInterface::processDirectoryPython(const QString& directory,
const QString& userGitHub,
const QString& branchGitHub) {
// Loop over all entries in directory
QDirIterator it(directory.toStdString().c_str());
QDirIterator it(directory);
while (it.hasNext()) {
QString nextDirectory = it.next();
@ -855,7 +855,7 @@ QDomElement TestRailInterface::processDirectoryXML(const QString& directory,
QDomElement result = element;
// Loop over all entries in directory
QDirIterator it(directory.toStdString().c_str());
QDirIterator it(directory);
while (it.hasNext()) {
QString nextDirectory = it.next();

View file

@ -366,7 +366,7 @@ void TestRunner::createSnapshotFolder() {
// Note that we cannot use just a `png` filter, as the filenames include periods
// Also, delete any `jpg` and `txt` files
// The idea is to leave only previous zipped result folders
QDirIterator it(_snapshotFolder.toStdString().c_str());
QDirIterator it(_snapshotFolder);
while (it.hasNext()) {
QString filename = it.next();
if (filename.right(4) == ".png" || filename.right(4) == ".jpg" || filename.right(4) == ".txt") {

View file

@ -36,7 +36,7 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) {
_ui.statusLabel->setText("");
_ui.plainTextEdit->setReadOnly(true);
setWindowTitle("Nitpick - v1.1");
setWindowTitle("Nitpick - v1.2");
// Coming soon to a nitpick near you...
//// _helpWindow.textBrowser->setText()

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 51b3237a2992bd449a58ade16e52d0e0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,207 @@
// AvatarExporter.cs
//
// Created by David Back on 28 Nov 2018
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
using UnityEngine;
using UnityEditor;
using System;
using System.IO;
using System.Collections.Generic;
public class AvatarExporter : MonoBehaviour {
public static Dictionary<string, string> UNITY_TO_HIFI_JOINT_NAME = new Dictionary<string, string> {
{"Chest", "Spine1"},
{"Head", "Head"},
{"Hips", "Hips"},
{"Left Index Distal", "LeftHandIndex3"},
{"Left Index Intermediate", "LeftHandIndex2"},
{"Left Index Proximal", "LeftHandIndex1"},
{"Left Little Distal", "LeftHandPinky3"},
{"Left Little Intermediate", "LeftHandPinky2"},
{"Left Little Proximal", "LeftHandPinky1"},
{"Left Middle Distal", "LeftHandMiddle3"},
{"Left Middle Intermediate", "LeftHandMiddle2"},
{"Left Middle Proximal", "LeftHandMiddle1"},
{"Left Ring Distal", "LeftHandRing3"},
{"Left Ring Intermediate", "LeftHandRing2"},
{"Left Ring Proximal", "LeftHandRing1"},
{"Left Thumb Distal", "LeftHandThumb3"},
{"Left Thumb Intermediate", "LeftHandThumb2"},
{"Left Thumb Proximal", "LeftHandThumb1"},
{"LeftEye", "LeftEye"},
{"LeftFoot", "LeftFoot"},
{"LeftHand", "LeftHand"},
{"LeftLowerArm", "LeftForeArm"},
{"LeftLowerLeg", "LeftLeg"},
{"LeftShoulder", "LeftShoulder"},
{"LeftToes", "LeftToeBase"},
{"LeftUpperArm", "LeftArm"},
{"LeftUpperLeg", "LeftUpLeg"},
{"Neck", "Neck"},
{"Right Index Distal", "RightHandIndex3"},
{"Right Index Intermediate", "RightHandIndex2"},
{"Right Index Proximal", "RightHandIndex1"},
{"Right Little Distal", "RightHandPinky3"},
{"Right Little Intermediate", "RightHandPinky2"},
{"Right Little Proximal", "RightHandPinky1"},
{"Right Middle Distal", "RightHandMiddle3"},
{"Right Middle Intermediate", "RightHandMiddle2"},
{"Right Middle Proximal", "RightHandMiddle1"},
{"Right Ring Distal", "RightHandRing3"},
{"Right Ring Intermediate", "RightHandRing2"},
{"Right Ring Proximal", "RightHandRing1"},
{"Right Thumb Distal", "RightHandThumb3"},
{"Right Thumb Intermediate", "RightHandThumb2"},
{"Right Thumb Proximal", "RightHandThumb1"},
{"RightEye", "RightEye"},
{"RightFoot", "RightFoot"},
{"RightHand", "RightHand"},
{"RightLowerArm", "RightForeArm"},
{"RightLowerLeg", "RightLeg"},
{"RightShoulder", "RightShoulder"},
{"RightToes", "RightToeBase"},
{"RightUpperArm", "RightArm"},
{"RightUpperLeg", "RightUpLeg"},
{"Spine", "Spine"},
{"UpperChest", "Spine2"},
};
public static string exportedPath = String.Empty;
[MenuItem("High Fidelity/Export New Avatar")]
public static void ExportNewAvatar() {
ExportSelectedAvatar(false);
}
[MenuItem("High Fidelity/Export New Avatar", true)]
private static bool ExportNewAvatarValidator() {
// only enable Export New Avatar option if we have an asset selected
string[] guids = Selection.assetGUIDs;
return guids.Length > 0;
}
[MenuItem("High Fidelity/Update Avatar")]
public static void UpdateAvatar() {
ExportSelectedAvatar(true);
}
[MenuItem("High Fidelity/Update Avatar", true)]
private static bool UpdateAvatarValidation() {
// only enable Update Avatar option if the selected avatar is the last one that was exported
if (exportedPath != String.Empty) {
string[] guids = Selection.assetGUIDs;
if (guids.Length > 0) {
string selectedAssetPath = AssetDatabase.GUIDToAssetPath(guids[0]);
string selectedAsset = Path.GetFileNameWithoutExtension(selectedAssetPath);
string exportedAsset = Path.GetFileNameWithoutExtension(exportedPath);
return exportedAsset == selectedAsset;
}
}
return false;
}
public static void ExportSelectedAvatar(bool usePreviousPath) {
string assetPath = AssetDatabase.GUIDToAssetPath(Selection.assetGUIDs[0]);
if (assetPath.LastIndexOf(".fbx") == -1) {
EditorUtility.DisplayDialog("Error", "Please select an fbx avatar to export", "Ok");
return;
}
ModelImporter importer = ModelImporter.GetAtPath(assetPath) as ModelImporter;
if (importer == null) {
EditorUtility.DisplayDialog("Error", "Please select a model", "Ok");
return;
}
if (importer.animationType != ModelImporterAnimationType.Human) {
EditorUtility.DisplayDialog("Error", "Please set model's Animation Type to Humanoid", "Ok");
return;
}
// store joint mappings only for joints that exist in hifi and verify missing joints
HumanDescription humanDescription = importer.humanDescription;
HumanBone[] boneMap = humanDescription.human;
Dictionary<string, string> jointMappings = new Dictionary<string, string>();
foreach (HumanBone bone in boneMap) {
string humanBone = bone.humanName;
string hifiJointName;
if (UNITY_TO_HIFI_JOINT_NAME.TryGetValue(humanBone, out hifiJointName)) {
jointMappings.Add(hifiJointName, bone.boneName);
}
}
if (!jointMappings.ContainsKey("Hips")) {
EditorUtility.DisplayDialog("Error", "There is no Hips bone in selected avatar", "Ok");
return;
}
if (!jointMappings.ContainsKey("Spine")) {
EditorUtility.DisplayDialog("Error", "There is no Spine bone in selected avatar", "Ok");
return;
}
if (!jointMappings.ContainsKey("Spine1")) {
EditorUtility.DisplayDialog("Error", "There is no Chest bone in selected avatar", "Ok");
return;
}
if (!jointMappings.ContainsKey("Spine2")) {
// if there is no UpperChest (Spine2) bone then we remap Chest (Spine1) to Spine2 in hifi and skip Spine1
jointMappings["Spine2"] = jointMappings["Spine1"];
jointMappings.Remove("Spine1");
}
// open folder explorer defaulting to user documents folder to select target path if exporting new avatar,
// otherwise use previously exported path if updating avatar
string directoryPath;
string assetName = Path.GetFileNameWithoutExtension(assetPath);
if (!usePreviousPath || exportedPath == String.Empty) {
string documentsFolder = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments);
if (!SelectExportFolder(assetName, documentsFolder, out directoryPath)) {
return;
}
} else {
directoryPath = Path.GetDirectoryName(exportedPath) + "/";
}
Directory.CreateDirectory(directoryPath);
// delete any existing fst since we agreed to overwrite it
string fstPath = directoryPath + assetName + ".fst";
if (File.Exists(fstPath)) {
File.Delete(fstPath);
}
// write out core fields to top of fst file
File.WriteAllText(fstPath, "name = " + assetName + "\ntype = body+head\nscale = 1\nfilename = " +
assetName + ".fbx\n" + "texdir = textures\n");
// write out joint mappings to fst file
foreach (var jointMapping in jointMappings) {
File.AppendAllText(fstPath, "jointMap = " + jointMapping.Key + " = " + jointMapping.Value + "\n");
}
// delete any existing fbx since we agreed to overwrite it, and copy fbx over
string targetAssetPath = directoryPath + assetName + ".fbx";
if (File.Exists(targetAssetPath)) {
File.Delete(targetAssetPath);
}
File.Copy(assetPath, targetAssetPath);
exportedPath = targetAssetPath;
}
public static bool SelectExportFolder(string assetName, string initialPath, out string directoryPath) {
string selectedPath = EditorUtility.OpenFolderPanel("Select export location", initialPath, "");
if (selectedPath.Length == 0) { // folder selection cancelled
directoryPath = "";
return false;
}
directoryPath = selectedPath + "/" + assetName + "/";
if (Directory.Exists(directoryPath)) {
bool overwrite = EditorUtility.DisplayDialog("Error", "Directory " + assetName +
" already exists here, would you like to overwrite it?", "Yes", "No");
if (!overwrite) {
SelectExportFolder(assetName, selectedPath, out directoryPath);
}
}
return true;
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c7a34be82b3ae554ea097963914b083f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1 @@
"C:\Program Files\Unity\Editor\Unity.exe" -quit -batchmode -projectPath %CD% -exportPackage "Assets/Editor" "avatarExporter.unitypackage"