1
0

Initial version

Signed-off-by: James Ketrenos <james_git@ketrenos.com>
This commit is contained in:
James Ketr 2024-06-24 16:50:06 -07:00
commit bf4956f5e0
14 changed files with 1411 additions and 0 deletions

4
.dockerignore Normal file
View File

@ -0,0 +1,4 @@
*
!Dockerfile
!scripts
!license_accepter.sh

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
AndroidSDK
*/node_modules
*/build
repos
flutter

0
AndroidSDK/.keep Normal file
View File

127
Dockerfile Normal file
View File

@ -0,0 +1,127 @@
# pull base image
FROM ubuntu:jammy AS android-sdk
#
# From https://github.com/thyrlian/AndroidSDK/blob/master/android-sdk/Dockerfile
#
# support multiarch: i386 architecture
# install Java
# install essential tools
ARG JDK_VERSION=17
RUN dpkg --add-architecture i386 && \
apt-get update && \
apt-get dist-upgrade -y && \
apt-get install -y --no-install-recommends libncurses5:i386 libc6:i386 libstdc++6:i386 lib32gcc-s1 lib32ncurses6 lib32z1 zlib1g:i386 && \
apt-get install -y --no-install-recommends openjdk-${JDK_VERSION}-jdk && \
apt-get install -y --no-install-recommends git wget unzip && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# download and install Gradle
# https://services.gradle.org/distributions/
ARG GRADLE_VERSION=8.7
ARG GRADLE_DIST=bin
RUN cd /opt && \
wget -q https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-${GRADLE_DIST}.zip && \
unzip gradle*.zip && \
ls -d */ | sed 's/\/*$//g' | xargs -I{} mv {} gradle && \
rm gradle*.zip
# download and install Kotlin compiler
# https://github.com/JetBrains/kotlin/releases/latest
ARG KOTLIN_VERSION=1.9.23
RUN cd /opt && \
wget -q https://github.com/JetBrains/kotlin/releases/download/v${KOTLIN_VERSION}/kotlin-compiler-${KOTLIN_VERSION}.zip && \
unzip *kotlin*.zip && \
rm *kotlin*.zip
# download and install Android SDK
# https://developer.android.com/studio#command-line-tools-only
ARG ANDROID_SDK_VERSION=11076708
ENV ANDROID_HOME /opt/android-sdk
RUN mkdir -p ${ANDROID_HOME}/cmdline-tools && \
wget -q https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_SDK_VERSION}_latest.zip && \
unzip *tools*linux*.zip -d ${ANDROID_HOME}/cmdline-tools && \
mv ${ANDROID_HOME}/cmdline-tools/cmdline-tools ${ANDROID_HOME}/cmdline-tools/tools && \
rm *tools*linux*.zip
# set the environment variables
ENV JAVA_HOME /usr/lib/jvm/java-${JDK_VERSION}-openjdk-amd64
ENV GRADLE_HOME /opt/gradle
ENV KOTLIN_HOME /opt/kotlinc
ENV PATH ${PATH}:${GRADLE_HOME}/bin:${KOTLIN_HOME}/bin:${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/cmdline-tools/tools/bin:${ANDROID_HOME}/platform-tools:${ANDROID_HOME}/emulator
# WORKAROUND: for issue https://issuetracker.google.com/issues/37137213
ENV LD_LIBRARY_PATH ${ANDROID_HOME}/emulator/lib64:${ANDROID_HOME}/emulator/lib64/qt/lib
# patch emulator issue: Running as root without --no-sandbox is not supported. See https://crbug.com/638180.
# https://doc.qt.io/qt-5/qtwebengine-platform-notes.html#sandboxing-support
ENV QTWEBENGINE_DISABLE_SANDBOX 1
# accept the license agreements of the SDK components
ADD license_accepter.sh /opt/
RUN chmod +x /opt/license_accepter.sh && /opt/license_accepter.sh $ANDROID_HOME
FROM android-sdk
# Seed image with necessary packages
RUN apt-get -q update \
&& DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \
ca-certificates \
curl \
git \
libglu1-mesa \
unzip \
wget \
xz-utils \
zip \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/{apt,dpkg,cache,log}
# Install Node 18
RUN wget -qO- https://deb.nodesource.com/setup_18.x | bash -
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y \
nodejs \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/{apt,dpkg,cache,log} \
&& npm install -g npm@latest
# install global packages
ENV NPM_CONFIG_PREFIX=/home/node/.npm-global
ENV PATH /scripts:/home/node/.npm-global/bin:$PATH
RUN npm i --unsafe-perm --allow-root -g npm@latest
WORKDIR /projects
VOLUME "project"
ENTRYPOINT [ "entrypoint" ]
CMD [ "shell" ]
# set our node environment, either development or production
# defaults to production, compose overrides this to development on build and run
ARG NODE_ENV=production
ENV NODE_ENV $NODE_ENV
# default to port 19006 for node, and 19001 and 19002 (tests) for debug
ARG PORT=19006
ENV PORT $PORT
EXPOSE $PORT 19001 19002
# Setup flutter SDK path
ENV PATH "/usr/bin/flutter/bin:$PATH"
# Setup Anrdoid SDK path
RUN git config --global --add safe.directory /usr/bin/flutter
#RUN apt-get update \
# && DEBIAN_FRONTEND=noninteractive apt-get install -y \
# clang++ \
# cmake \
# ninja-build \
# pkg-config \
# && apt-get clean \
# && rm -rf /var/lib/apt/lists/{apt,dpkg,cache,log}
COPY /scripts /scripts

75
README.md Normal file
View File

@ -0,0 +1,75 @@
# Android Development Container
This sets up development containers for Expo and Flutter using
the Android SDK. The Dockerfile is derived from parts of [thyrlian/android-sdk](https://github.com/thyrlian/AndroidSDK), with expo and flutter development
infrastructure configured as well.
## Build the Android SDK base container
This will seed the base container with an initial version of the Android SDK.
After it is created, the SDK is copied to a volume bind mount on the host to
allow multiple containers to use the same base SDK, as well as to perform
SDK updates, upgrades, and additional package installations.
```bash
docker compose build
```
## Seed the flutter repository
```
git clone https://github.com/flutter/flutter.git
```
## Copy the base SDK to the mounted 'sdk' directory
```bash
docker compose run -it --rm \
android-dev-container -- \
bash -c 'cp -a $ANDROID_HOME/. /sdk/'
```
## Install packages into SDK
The following will install Android platform 35:
```bash
ANDROID_PLATFORM=35
docker compose run -it --rm \
android-dev-container -- \
sdkmanager \
"build-tools;${ANDROID_PLATFORM}.0.0" \
"platforms;android-${ANDROID_PLATFORM}"
```
At this point, ./AndroidSDK contains the latest SDK. The docker-compose.yml
for the android-dev-container will mount ./AndroidSDK to /opt/android-sdk.
## To seed a new project (once per project)
```bash
PROJECT=foo
PROJECT_TYPE=expo # flutter
./seed.sh "${PROJECT_TYPE}" "${PROJECT}"
```
## To develop
```bash
PROJECT=foo
PROJECT_TYPE=expo
./develop.sh "${PROJECT_TYPE}" "${PROJECT}"
```
## To access the shell of the running project
```bash
PROJECT=foo
./shell.sh "${PROJECT}"
```
## To stop development container
```bash
docker compose stop "${PROJECT}"
```

31
develop.sh Executable file
View File

@ -0,0 +1,31 @@
#!/bin/bash
fail() {
echo "$*" >&2
exit 1
}
declare project_type=$1
declare project=$2
if [[ "${project}" == "" ]] || [[ "${project_type}" == "" ]]; then
echo "usage: ./launch expo|flutter PROJECT" >&2
exit 1
fi
if [[ ! -d "projects/${project}" ]]; then
if ! mkdir -p "projects/${project}"; then
fail "mkdir -p projects/${project}"
fi
fi
declare full_path=$(realpath "projects/${project}")
if ! docker compose run -it --rm \
--name "${project}-seed" \
-v $(pwd)/scripts:/scripts \
-v "${full_path}:/projects/${project}" \
android-dev-container -- \
develop \
"${project_type}" \
"${project}"; then
fail "Unable to launch ${project_type} develop container for ${project}"
fi

27
docker-compose.yml Normal file
View File

@ -0,0 +1,27 @@
services:
android-dev-container:
build:
context: .
args:
- NODE_ENV=development
environment:
- NODE_ENV=development
tty: true
ports:
- '19006:19006'
- '19001:19001'
- '19002:19002'
volumes:
- ./react_native_app:/opt/react_native_app/app:delegated
- ./react_native_app/package.json:/opt/react_native_app/package.json
- ./react_native_app/package-lock.json:/opt/react_native_app/package-lock.json
- ./flutter:/usr/bin/flutter
- notused:/opt/react_native_app/app/node_modules
- ./repos:/opt/repos
- ./AndroidSDK:/opt/android-sdk:ro
- ./scripts:/scripts:rw
healthcheck:
disable: true
volumes:
notused:

52
license_accepter.sh Executable file
View File

@ -0,0 +1,52 @@
#!/bin/bash
check_android_env_var() {
if [ "$#" -lt 1 ]; then
if [ -z "${ANDROID_HOME}" ]; then
echo "Please either set ANDROID_HOME environment variable, or pass ANDROID_HOME directory as a parameter"
exit 1
else
ANDROID_HOME="${ANDROID_HOME}"
fi
else
ANDROID_HOME=$1
fi
echo "ANDROID_HOME is at $ANDROID_HOME"
}
accept_all_android_licenses() {
ANDROID_LICENSES="$ANDROID_HOME/licenses"
if [ ! -d $ANDROID_LICENSES ]; then
echo "Android licenses directory doesn't exist, creating one..."
mkdir -p $ANDROID_LICENSES
fi
accept_license_of android-googletv-license 601085b94cd77f0b54ff86406957099ebe79c4d6
accept_license_of android-sdk-license 8933bad161af4178b1185d1a37fbf41ea5269c55
accept_license_of android-sdk-license d56f5187479451eabf01fb78af6dfcb131a6481e
accept_license_of android-sdk-license 24333f8a63b6825ea9c5514f83c2829b004d1fee
accept_license_of android-sdk-preview-license 84831b9409646a918e30573bab4c9c91346d8abd
accept_license_of android-sdk-preview-license 504667f4c0de7af1a06de9f4b1727b84351f2910
accept_license_of google-gdk-license 33b6a2b64607f11b759f320ef9dff4ae5c47d97a
accept_license_of intel-android-extra-license d975f751698a77b662f1254ddbeed3901e976f5a
}
accept_license_of() {
local license=$1
local content=$2
local file=$ANDROID_LICENSES/$license
if [ -f $file ]; then
if grep -q "^$content$" $file; then
echo "$license: $content has been accepted already"
else
echo "Accepting $license: $content ..."
echo -e $content >> $file
fi
else
echo "Accepting $license: $content ..."
echo -e $content > $file
fi
}
check_android_env_var "$@"
accept_all_android_licenses

0
repos/.keep Normal file
View File

89
scripts/entrypoint Executable file
View File

@ -0,0 +1,89 @@
#!/bin/bash
script_path=$(dirname "${0}")
. "${script_path}/lib/common"
arguments=(
"h|help#This help text."
)
declare -i parse_error=0
declare command=""
#
# Parse command line arguments
#
if ! parse_arguments "${@}"; then
parse_error=1
fi
#
# Process command line options
#
eval set -- "${opts}"
remaining=$#
while (( $# > 0 )); do
if [[ ${remaining} == 0 ]]; then
fail "case statement is not shifting off values correctly."
fi
remaining=$((remaining-1))
# Uncomment to help debug case statement:
# echo "Processing: \$1='$1' \$2='$2'"
case "${1}" in
-h|--help) # Help / usage
usage_extra="[COMMAND [OPTIONS]]"
default_usage
exit 0
;;
--)
shift
break
;;
esac
shift
done
declare command
declare -a options=()
if (( ${#} > 0 )); then
command="${1}"
shift
options=("${@}")
fi
if [[ "${command}" == "" ]]; then
echo "COMMAND must be supplied." >&2
exit 1
fi
if (( parse_error )); then
exit 1
fi
case "${command}" in
*bash|shell)
/bin/bash "${options[@]}"
exit $?
;;
develop)
project_type="${1}"
project="${2}"
cd "/projects/${project}"
case "${project_type}" in
"expo")
echo "Launching web app in ${project}"
npm run web
;;
"flutter")
echo "Launching terminal in ${project}"
/bin/bash
;;
esac
;;
seed-project)
seed-project "${options[@]}"
exit $?
;;
*)
fail "Invalid command '${command}'"
esac

814
scripts/lib/common Normal file
View File

@ -0,0 +1,814 @@
#!/bin/bash
# fail ERROR-MESSAGE
#
# Outputs parameters (ERROR-MESSAGE) passed to 'fail' to stderr
#
# If a function 'fail_cleanup' exists, execute it after outputting
# error message. This allows scripts to perform any necessary script
# specific cleanup on failure.
#
# If 'no_exit' is set, or 'script_name' is 'bash',
# do not exit the process and return an error. This lets you chain
# error handlers or run the 'irepo' functions from a bash command line
# via:
# . /opt/scripts/lib/irepo
#
declare -g fail_failed=0
declare -g fail_min_stack=1
declare -g fail_skip_continue=0
declare -g fail_skip_filepath="/home/user/.${script_name}.skip_continue_target"
fail() {
local tmp=$?
if (( fail_failed )); then
exit 1
fi
fail_failed=${tmp}
# On exit 1 from fail, trap is called again. We do not want to display
# the stack frame for fail a second time, so set the fail_min_stack
# as the limit.
if (( ${#} != 0 )); then
echo -e "$*" | while read -r line; do
echo "FAIL: ${line}" >&2
done
fail_min_stack=0
else
echo -e "FAIL: ERR trap triggered: ${script_name} ${full_command}" >&2
fail_min_stack=1
fi
if (( fail_skip_continue )); then
echo ${BASH_LINENO[0]} > "${fail_skip_filepath}"
fi
# https://bashwizard.com/function-call-stack-and-backtraces/
declare CALLSTACK=
if (( ${#FUNCNAME[*]} > fail_min_stack )); then
CALLSTACK=$(local i ;
for (( i=0; i<${#FUNCNAME[*]}; i++ )); do
echo "FAIL: ${FUNCNAME[$i]}@${BASH_SOURCE[$i]}:${BASH_LINENO[$i-1]}"
done)
fi
echo "${CALLSTACK}" >&2
if [[ "${FAILURE_TO}" != "" ]]; then
echo "Attempting to email ${FAILURE_TO} of failure." >&2
declare _dir=$(pwd)
_temp=$(mktemp -d)
_filename="error-$(date +"%Y%m%d").log"
{
if [[ -e "${repositories}/.lock" ]]; then
echo -e "Lockfile contents:\n"
cat "${repositories}/.lock"
else
echo -e "No lockfile present during failure."
fi
echo ""
echo -e "Callstack:\n"
echo "${CALLSTACK}"
echo ""
echo -e "Environment:\n"
export
echo ""
} >> "${_temp}/${_filename}"
cd "${IREPO_HOME}/server"
if [[ -e "${log_file}" ]]; then
extra_attachments="--files=${log_file}"
else
extra_attachments=""
fi
export NODE_EXTRA_CA_CERTS="/usr/local/share/ca-certificates/IntelSHA256RootCA-base64.crt"
npm run mailer -- \
--to="${FAILURE_TO}" \
--from="${FAILURE_FROM:-james.p.ketrenos@intel.com}" \
--type="failure" \
--files="${_temp}/${_filename}" \
"${extra_attachments}"
cd "${_dir}"
rm -rf "${_temp}"
fi
# If we are in an interactive shell, do not call exit or the shell will
# terminate.
if [[ "${-}" =~ .*i.* ]]; then
kill -INT $$
fi
# execute fail_cleanup if it is declared
if [[ $(type -t fail_cleanup) == function ]]; then
fail_cleanup
fi
exit 1
}
trap "fail" ERR
fail_on_exit() {
if (( $# == 0 )); then
fail "fail_on_exit must be provided a return code"
fi
local ret=$1
local message=$2
message="${message:-unspecified command}"
if [[ ${ret} -ne 0 ]]; then
fail "${message} failed: ${ret}"
return ${ret}
fi
}
int() { [ "$1" -eq "$1" ] || fail "$1 is not an integer." ; echo $1 ; }
declare -g skip_continue_target=0
# skip_continue
#
# If called, skip_continue will enable line tracking in the fail ERR trap.
# On failure, the file '/home/user/.${script_name}.skip_continue_target'
# On exectuion, skip_continue will check the lineno contained in that
# file and only return SUCCESS (0) if the current Bash line number
# of the caller >= skip_continue_target.
#
# NOTE: This will not work with nested functions calling skip_continue
# from functions not in the main script.
skip_continue() {
fail_skip_continue=1
if (( fail_failed )); then
show_vals fail_failed
exit ${fail_failed}
fi
if (( ! skip_continue_target )) && [[ -f "${fail_skip_filepath}" ]]; then
skip_continue_target=$(cat "${fail_skip_filepath}")
fi
# Get the line of the skip_continue call
local line_no=${BASH_LINENO[0]}
line_no=$((line_no+1))
# Read two lines to see if the skip_continue has the command on
# the actual line, or on the following line. Set "line" to that
# line for display in the Running or Skipped output
mapfile lines < <(head -n ${line_no} "${BASH_SOURCE[1]}"| tail -n 2)
if [[ "${lines[0]}" =~ \&\&[[:space:]\\]*\#? ]]; then
line="${lines[1]}"
else
line="${lines[0]%*skip_continue &&}"
fi
if (( line_no >= skip_continue_target )); then
echo "Running (${line_no}): ${line%$'\n'}"
return 0
else
echo "Skipping (${line_no}): ${line%$'\n'}"
return 1
fi
}
# array_to_csv ARRAY [DELIM]
#
array_to_csv() {
declare -n _arr="${1}"
local delim="${2}"
local joined
delim=${delim:-,}
printf -v joined "%s${delim}" "${_arr[@]}"
joined="${joined%"${delim}"}"
joined="${joined#"${delim}"}"
echo "${joined}"
}
# csv_to_array ARRAY CSV [DELIM]
#
csv_to_array() {
declare -n _arr="${1}"
_arr="${2}"
declare _delim="${3:-,}"
mapfile -d "${_delim}" -t _arr <<< "${_arr}"
count=${#_arr[@]}
_arr[count-1]="${_arr[count-1]%$'\n'}"
}
# pop TARGET ARRAY
#
# removes the first element from ARRAY and stores in TARGET
pop() {
declare -n _target="${1}"
declare -n _arr="${2}"
if (( ${#_arr[@]} == 0 )); then
_target=""
return 1
fi
_target="${_arr[0]}"
_arr=("${_arr[@]:1}")
return 0
}
# parse_arguments COMMAND-LINE-ARGUMENTS
#
# Converts 'arguments' variable to long and short parameter definitions
#
# Parses COMMAND-LINE-ARGUMENTS and sets 'opts' as the parsed parameters
# to be used by caller in switch statement.
#
# NOTE:
# Optional parameters must be passed to the command line as
# '-param=VALUE' and not as '-param VALUE' or the optional parameter
# value will not be parsed and will be moved to unparsed parameters.
#
# Correct:
# app -has-optional=5 -other-param
#
# Incorrect:
# app -has-optional 5 -other-param
#
# Non-optional parameters can use space, however for consistency it is
# recommended that usage help show users to use '=' even for non-optional
# parameters.
#
# See 'arg_example' later in this file for sample usage.
#
parse_arguments() {
local arg_debug=0
if [[ ${#arguments[@]} == 0 ]]; then
fail "No command line arguments options in '\$arguments'"
fi
local sopts
local lopts
local arg
local _arguments
local mandatory=()
local valid=()
for arg in "${arguments[@]}"; do
arg="${arg%%\#*}" # Trim off any Help text
local -a argument_set
csv_to_array argument_set "${arg}" '|' # Convert to array
for argument in "${argument_set[@]}"; do
trimmed=${argument%:} # Trim off 'value required' indicator
trimmed=${trimmed%:} # Trim off 'value optional' indicator
if [[ "${argument:0:1}" == '*' ]]; then # Check if mandatory
argument="${argument:1}" # Strip off '*'
trimmed="${trimmed:1}" # Strip off '*'
mandatory_arg="${argument_set[*]//:/ }" # Remove :+ from argument_set
mandatory_arg="${mandatory_arg//$'*'}" # Remove '*'+ from argument_set
mandatory+=("${mandatory_arg}") # Add argument_set to mandatory
fi
valid+=("${trimmed}")
if [[ ${#trimmed} == 1 ]]; then
sopts="$argument${sopts}"
else
if [[ "${lopts}" == "" ]]; then
lopts=${argument}
else
lopts=${lopts},${argument}
fi
fi
done
done
(( arg_debug )) && echo "lopts: ${lopts}" >&2
(( arg_debug )) && echo "sopts: ${sopts}" >&2
# Only accept exact parameters, do not let getopt try and "fit"
for opt in "${@}"; do
if [[ ! "${opt}" =~ ^- ]]; then
continue
fi
if [[ "${opt}" == "--" ]]; then
break
fi
opt=${opt#-}
opt=${opt%%=*}
if [[ ! " ${valid[*]} " =~ [[:space:]]${opt}[[:space:]] ]]; then
echo "ERROR: '-${opt}' is not a valid parameter." >&2
return 1
fi
done
opts=$(getopt \
-a \
--longoptions "${lopts}" \
--name "$(basename "$0")" \
--options "${sopts}" \
-- "$@"
)
if [[ $? -ne 0 ]]; then
(( arg_debug )) && echo "opts: ${opts}"
fail "\nSee '$(basename "${0}") -h' for valid options.\n"
fi
# Enforce mandatory options
for arg in "${mandatory[@]}"; do
provided=0
for opt in ${opts}; do
if [[ "${opt}" == "--" ]]; then
break
fi
for acceptable in ${arg}; do
if [[ "${opt}" =~ ^-\-?"${acceptable}"$ ]]; then
provided=1
break
fi
done
done
if (( ! provided )); then
local last_arg=${arg% }
last_arg=${last_arg##* }
echo "ERROR: '-${last_arg}=' is a mandatory requirement and must be supplied." >&2
return 1
fi
done
return 0
}
# Example function demonstrating argument declaration and parsing.
#
# You can copy this into your entry script, change the name,
# and then call it in the start of your application passing in all
# of the command line arguments, for example:
#
# copy arg_example => process_arguments
# process_arguments ${@}
#
# And then adapt as fit for your application's parameters.
#
# To run the arg_example script, you can run irepo with
# arg_example_show set to a value. For example:
#
# arg_example_show=1 irepo -h -lp -required-parameter=5
#
arg_example() {
arguments=(
"h|help#This help text."
"lp" "long-parameter"
"rp:" "required-parameter:"
"hop::" "has-optional-parameter:"
)
parse_arguments "${@}"
eval set -- "$opts"
remaining=$#
while (( $# > 0 )); do
if [[ ${remaining} == 0 ]]; then
fail "case statement is not shifting off values correctly."
fi
remaining=$((remaining-1))
# Uncomment to help debug case statement:
# echo "Processing: \$1='$1' \$2='$2'"
case "${1}" in
# NOTE: Short options only have a single dash
-h|--help)
echo "...show help..."
;;
# NOTE: Long options always have a double dash, even though the
# application reads them from the command-line as a single dash
--lp|--long-parameter)
echo "...do work for long-parameter..."
;;
--rp|--required-parameter)
value=${2}
shift # Remove the argument from the processing list
echo "...do work for 'required-parameter=${value}'..."
;;
--hop|--has-optional-parameter)
value=default
if [[ "${2}" != "" ]]; then
value="${2}"
fi
shift # Remove the optional argument from the processing list
echo "...do work for 'has-optional-parameter=${value}'..."
;;
--)
shift
break
;;
esac
shift
done
}
# show_vals VARIABLES
#
# Helper function for use in debugging. Typical usage:
#
# show_vals [OPTIONS] VARIABLE1 VARIABLE2 ... VARIABLEn
#
# Available options: -json -deshadow
# -json Outputs as JSON
# -deshadow Remove shadow_ prefix from any variable name
#
show_vals() {
declare -i json=0
declare -i deshadow=0
# Eventually replace this with actual arg parsing, but since
# only two options are possible, just check twice to parse both
if [[ "${1}" == "-json" ]]; then
json=1
shift
fi
if [[ "${1}" == "-deshadow" ]]; then
deshadow=1
shift
fi
if [[ "${1}" == "-json" ]]; then
json=1
shift
fi
if [[ "${1}" == "-deshadow" ]]; then
deshadow=1
shift
fi
local var
local value
declare -i first=1
if (( json )); then
echo "{"
fi
for var in "${@}"; do
if (( deshadow )); then
var_name="${var/shadow_}"
else
var_name="${var}"
fi
if (( first )); then
first=0
else
if ((json )); then
echo ","
fi
fi
if [[ "${!var}" == "" ]]; then
if (( json )); then
echo -n " \"${var_name}\": undefined"
else
echo "${var_name} = '' or unset"
fi
continue
fi
value=$(sed -nE "s#declare -a ${var}=\((.*)\)#\1#p" < \
<(declare -p "${var}"))
if (( json )); then
if [[ "${value}" == "" ]]; then
echo -n " \"${var_name}\": \"${!var}\""
else
eval declare -A values=(${value})
echo -n " \"${var_name}\": ["
declare -i first_arr=1
for item in "${values[@]}"; do
if (( first_arr )); then
first_arr=0
else
echo -n ", "
fi
echo -n "\"${item}\""
done
echo -n "]"
fi
else
if [[ "${value}" == "" ]]; then
echo "${var_name} = ${!var}"
else
echo "${var_name}[] = ${value}"
fi
fi
done
if (( json )); then
echo -e "\n}"
fi
}
export yes
# ask VAR OPTS PROMPT
#
# Will display PROMPT OPTS ? with OPTS split by | and default wrapped
# in (). VAR is set to selected option.
#
# If OPTS contains a capital letter, that is used as the default if
# ENTER is pressed.
#
# Usage:
# ask ret yN "Are you sure"
# if [[ "${ret}" == "n" ]]; then echo No; fi
# if [[ "${ret}" == "y" ]]; then echo Yes; fi
#
# NOTE: If 'yes' is set in the enviornment, and 'y' is in the option
# then 'y' value will be immediately returned after the prompt is shown.
#
# Will prompt: Are you sure y|(N)?
# If neither y, n, or ENTER is pressed, the prompt will loop.
# 'res' contains the character the user selected (lowercase)
ask() {
if [[ "${1}" == "-h" ]] || (( $# < 3 )); then
fail "ask RETVAR OPTS PROMPT"
fi
declare -n _ret="${1}"
local opts="${2}a" # Ny - N is default; ynC - C is default; Ynm - Y is default
shift 2
local prompt="${*}"
local index
local opts_str
local default
# Loop through each character in opts and build opt_str
# for display to read prompt.
while [[ "${opts:${index}:1}" != "" ]]; do
local c=${opts:${index}:1}
index=$((index+1))
if [[ ${c} =~ [A-Z] ]]; then
if [[ "${default}" != "" ]]; then
fail "Only one character can be default: ${opts} ${default} ${c}"
fi
if [[ "${opts_str}" != "" ]]; then
opts_str="${opts_str}|(${c})"
else
opts_str="(${c})"
fi
default=${c,,}
else
if [[ "${opts_str}" != "" ]]; then
opts_str="${opts_str}|${c}"
else
opts_str="${c}"
fi
fi
done
if (( yes )) && [[ ${opts} =~ [yY] ]]; then
# echo "${FUNCNAME[1]}@${BASH_SOURCE[1]}: ${prompt} ${opts_str}? y"
echo "${prompt} ${opts_str}? y"
_ret="y"
return 0
fi
local REPLY
while read -n1 -r -p "${prompt} ${opts_str}? " REPLY; do
case $REPLY in
"a")
fail "Abort"
;;
"")
if [[ "${default}" != "" ]]; then
_ret="${default,,}"
return 0
fi
;;
*)
echo ""
if [[ ${REPLY,,} =~ [${opts,,}] ]]; then
_ret="${REPLY,,}"
return 0
fi
;;
esac
done
}
# default_usage
#
# Display default usage based on ${arguments}
#
# 'arguments' is an ARRAY of strings.
#
# Each string is of the form [*]NAME[:](|NAME2[:])#Help text.
# If the leading '*' is present, this parameter is mandatory.
# If a name is followed by a single ':", that parameter takes a value.
# If name is followed by two colons ('::') the parameter is optional.
# |NAME2[:] provides the ability to list both short and long parameter
# options for a given parameter.
# The help text is displayed right aligned.
# If a variable NAME or shadow_NAME exists and is set, its value is
# displayed in the Help text as the 'Default'.
#
default_generate_help_text() {
for arg in "${@}"; do
local _help
local _default=
local _help_text="${arg#*\#}" # Strip ^.*# from _help_text
if [[ "${_help_text}" == "${arg}" ]]; then
_help_text=""
fi
mapfile -t _help_text < <(fold -s -w 50 <<< "${_help_text}")
if [[ "${arg:0:1}" == '*' ]]; then # Check if is_mandatory
arg="${arg:1}" # Strip off '*'
is_mandatory=1
else
is_mandatory=0
fi
arg="${arg%%\#*}" # Trim off any help text
local -a argument_set
csv_to_array argument_set "${arg}" '|' # Convert to array
for argument in "${argument_set[@]}"; do
if (( ${#argument_set[@]} > 1 )); then
argument_value_name=${argument_set[-1]}
else
argument_value_name=${argument}
fi
if [[ "${argument}" == "${argument_set[0]}" ]]; then
first_argument=1
else
first_argument=0
fi
if [[ "${argument}" == "${argument_set[-1]}" ]]; then
last_argument=1
else
last_argument=0
fi
if (( ${#argument_set[@]} > 1 )); then
more_than_one_argument=1
else
more_than_one_argument=0
fi
# If takes option, trim : off end of names
if [[ "${argument:0-1}" == ":" ]]; then
takes_value=1
argument_value_name=${argument_value_name:0:-1}
argument="${argument%:}" # Trim off the trailing :
else
takes_value=0
fi
# If option is optional, trim : off end of names
if [[ "${argument:0-1}" == ":" ]]; then
takes_value=0
takes_optional_value=1
argument_value_name=${argument_value_name:0:-1}
argument="${argument%:}" # Trim off the trailing :
else
takes_optional_value=0
fi
if (( is_mandatory )) && (( takes_optional_value )); then
fail "'${argument}' Mandatory (*) requirements can not take optional values (::)"
fi
# If this is the first argument, initialize _help from first item
# in _help_text. _default="". _default will be set later
# if this argument takes parameters.
if (( first_argument )); then
pop _help _help_text
_default=""
showed_help=0
fi
if (( takes_value )) || (( takes_optional_value )); then
# If a shadow_ variant exists, use that as the default,
# otherwise use the argument_value_name (with - changed to _)
declare -n _shadow="shadow_${argument_value_name//-/_}"
if [[ "${_shadow}" == "" ]]; then
declare -n _shadow="${argument_value_name//-/_}"
fi
# Make the display name for a value for
# foo-bar-rabbit into RABBIT
argument_value_name=${argument_value_name^^}
argument_value_name=${argument_value_name##*-}
# First argument. Initializes _default.
if (( first_argument )); then
if (( is_mandatory )); then
_default="Mandatory: ${argument_value_name} option must be provied."
else
if [[ "${_shadow}" != "" ]]; then
_default="Default: $(array_to_csv _shadow)"
else
_default="Default: no default"
fi
fi
fi
# If this is the last argument in the set and the current help
# text is empty and there is no more _help_text, set the
# _help_text to default
if (( last_argument )) &&
[[ "${_help}" == "" ]] &&
(( ${#_help_text[@]} == 0 )) ; then
mapfile -t _help_text < <(fold -s -w 50 <<< "${_default}")
_default=""
pop _help _help_text
fi
if (( takes_optional_value )); then
equals="[=${argument_value_name}]"
else
equals="=${argument_value_name}"
fi
if (( first_argument )); then
# If there is more than one argument in the set, add a | to the
# output and do not display _default
if (( more_than_one_argument )); then
printf " %-30s %s\n" \
"-${argument}${equals}|" \
"${_help}"
showed_help=1
else
printf " %-30s %s\n" \
"-${argument}${equals}" \
"${_help}"
showed_help=1
fi
else # (( ! first_argument ))
# If not last optional argument, add a | to the output and do
# not display _help
if (( ! last_argument )); then
printf " %-30s %s\n" \
"-${argument}${equals}|" \
"${_help}"
showed_help=1
else
printf " %-30s %s\n" \
"-${argument}${equals}" \
"${_help}"
showed_help=1
fi
fi
else # (( ! takes_value )) && (( ! takes_optional_value ))
if (( first_argument )); then
option_string=""
# If there is more than one argument in the set, add a | to the
# output
if (( more_than_one_argument )); then
option_string="${option_string}-${argument}|"
else # (( last_argument ))
printf " %-30s %s\n" "-${argument}" "${_help}"
showed_help=1
fi
else # (( ! first_argument ))
# If last non-optional argument, display help
if (( last_argument )); then
printf " %-30s %s\n" "${option_string}-${argument}" "${_help}"
showed_help=1
else
option_string="${option_string}-${argument}|"
fi
fi
fi
if (( showed_help )); then
#show_vals argument argument_set first_argument last_argument more_than_one_argument _help _help_text _default is_mandatory takes_value takes_optional_value showed_help _shadow
pop _help _help_text
fi
done # done for 'arg in "${argument_set[@]}"''
# If there is still a help string, display it.
while [[ "$_help" != "" ]] || (( ${#_help_text[@]} > 0 )); do
printf " %-30s %s\n" "" "${_help}"
if ! pop _help _help_text; then
if [[ "${_default}" == "" ]]; then
break
fi
mapfile -t _help_text < <(fold -s -w 50 <<< "${_default}")
_default=""
pop _help _help_text
fi
done
_help=""
_default=""
done
}
default_usage() {
local _mandatory=()
for arg in "${arguments[@]}"; do
if [[ "${arg:0:1}" == '*' ]]; then # Check if is_mandatory
arg="${arg%#*}" # Strip off help
arg="${arg:1:-1}" # Strip off '*'
csv_to_array arg "${arg}" '|'
_mandatory+=("${arg[-1]}")
fi
done
local _text="Usage: ${script_name} [OPTIONS] ${usage_extra}"
for _arg in "${_mandatory[@]}"; do
_text="${_text} -${_arg}=${_arg^^}"
done
mapfile -t _text < <(fold -s -w 72 <<< "${_text}")
first_line=1
for _line in "${_text[@]}"; do
if (( first_line )); then
echo -e "\n${_line}"
first_line=0
else
echo " ${_line}"
fi
done
echo -e "\nOptions:\n"
default_generate_help_text "${arguments[@]}"
echo ""
}

125
scripts/seed-project Executable file
View File

@ -0,0 +1,125 @@
#!/bin/bash
script_path=$(dirname "${0}")
. "${script_path}/lib/common"
arguments=(
"h|help#This help text."
"u:|user:#User ID to chown files to"
"g:|group:#Group ID to chown files to"
)
declare -i parse_error=0
declare project=""
declare -i group=1000
declare -i user=1000
#
# Parse command line arguments
#
if ! parse_arguments "${@}"; then
parse_error=1
fi
#
# Process command line options
#
eval set -- "${opts}"
remaining=$#
while (( $# > 0 )); do
if [[ ${remaining} == 0 ]]; then
fail "case statement is not shifting off values correctly."
fi
remaining=$((remaining-1))
# Uncomment to help debug case statement:
# echo "Processing: \$1='$1' \$2='$2'"
case "${1}" in
-h|--help) # Help / usage
usage_extra="[PROJECT]"
default_usage
exit 0
;;
-u|--user)
user=${1}
shift
;;
-g|--group)
group=${1}
shift
;;
--)
shift
break
;;
esac
shift
done
if (( ${#} > 0 )); then
project_type=$1
shift
fi
if (( ${#} > 0 )); then
project=$1
shift
fi
if [[ "${project_type}" == "" ]]; then
echo "PROJECT-TYPE must be supplied." >&2
exit 1
fi
if [[ "${project}" == "" ]]; then
echo "PROJECT must be supplied." >&2
exit 1
fi
if (( parse_error )); then
exit 1
fi
case "${project_type}" in
"expo")
if [[ -e "${project}/package.json" ]]; then
fail "${project} already exists."
fi
#
# 'npx expo init' was replaced with 'npx create-expo-app'
#
echo "Seeding expo project ${project} in $(pwd)/${project}..."
if ! npx create-expo-app "${project}"; then
fail "npx create-expo-app ${project}"
fi
cd "${project}"
if ! npx expo install react-native-web react-dom @expo/webpack-config; then
fail "npx expo install react-native-web react-dom @expo/webpack-config"
fi
echo "Installing node packages..."
if ! npm install; then
fail "npm install"
fi
if ! chown -R ${user}:${group} .; then
fail "chown -R ${user}:${group} ."
fi
;;
"flutter")
ls -altr /usr/bin/flutter
declare version="flutter_linux_3.22.2-stable.tar.xz"
if [[ ! -e "/usr/bin/flutter/${version}" ]]; then
if ! wget -O "/usr/bin/flutter/${version}" "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/${version}"; then
fail "wget ${version}"
fi
fi
if [[ ! -x "/usr/bin/flutter/flutter" ]]; then
if ! tar -xf "/usr/bin/flutter/${version}" -C /usr/bin/; then
fail "tar ${version}"
fi
fi
git config --global --add safe.directory /usr/bin/flutter
;;
esac

35
seed.sh Executable file
View File

@ -0,0 +1,35 @@
#!/bin/bash
fail() {
echo "$*" >&2
exit 1
}
declare project_type=$1
declare project=$2
if [[ "${project}" == "" ]] || [[ "${project_type}" == "" ]]; then
echo "usage: ./launch expo|flutter PROJECT" >&2
exit 1
fi
if [[ ! -d "projects/${project}" ]]; then
if ! mkdir -p "projects/${project}"; then
fail "mkdir -p projects/${project}"
fi
fi
declare full_path=$(realpath "projects/${project}")
if ! docker compose run -it --rm \
--name "${project}-seed" \
-v $(pwd)/scripts:/scripts \
-v "${full_path}:/projects/${project}" \
android-dev-container -- \
seed-project \
"${project_type}" \
-u $(id -u) \
-g $(id) \
"${project}"; then
fail "Unable to run seed-project for ${project}"
else
echo "Project '${project}' now exists in projects/${project}"
fi

27
shell.sh Executable file
View File

@ -0,0 +1,27 @@
#!/bin/bash
fail() {
echo "$*" >&2
exit 1
}
declare project_type=$1
declare project=$2
if [[ "${project}" == "" ]] || [[ "${project_type}" == "" ]]; then
echo "usage: ./launch expo|flutter PROJECT" >&2
exit 1
fi
if [[ ! -d "projects/${project}" ]]; then
if ! mkdir -p "projects/${project}"; then
fail "mkdir -p projects/${project}"
fi
fi
declare full_path=$(realpath "projects/${project}")
if ! docker compose exec -it \
"${project}" \
shell; then
fail "Unable to launch shell in ${project}"
fi