commit bf80f5e5ec26b5db7956a11b277f5bc0846326b2 Author: James Ketrenos Date: Mon Jun 24 16:50:06 2024 -0700 Initial version Signed-off-by: James Ketrenos diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..09dfdd0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +* +!Dockerfile +!scripts +!license_accepter.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3358261 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +AndroidSDK +*/node_modules +*/build +repos +flutter +projects/** ++projects/.keep diff --git a/AndroidSDK/.keep b/AndroidSDK/.keep new file mode 100644 index 0000000..e69de29 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..27d6068 --- /dev/null +++ b/Dockerfile @@ -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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..05d1894 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Files: license_accepter.sh - Copyright 2016-2024 Jing Li from https://github.com/thyrlian/AndroidSDK + Files: * - Copyright 2024 James Ketrenos + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..242eedf --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +# 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. + +```bash +docker compose build +``` + +This will build the base 'android-sdk-manager' container as well as the unseeded +'android-dev-container'. + +Inside the 'anrdoid-sdk-manager' container the directory '/sdk' will contain the original 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. The initial +seeding of the AndroidSDK on the host is performed via: + +```bash +docker compose run -it --rm \ + android-sdk-manager \ + init +``` + +## Install packages into SDK + +The following will install Android platform 35. + +```bash +ANDROID_PLATFORM=35 +docker compose run -it --rm \ + android-sdk-manager -- \ + sdkmanager \ + "build-tools;${ANDROID_PLATFORM}.0.0" \ + "platforms;android-${ANDROID_PLATFORM}" \ + "platform-tools" +``` + +At this point, ./AndroidSDK contains the latest SDK. The docker-compose.yml +for the android-dev-container will mount ./AndroidSDK to /opt/android-sdk. + +### 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/' +``` + +## Seed the flutter repository + +``` +git clone https://github.com/flutter/flutter.git +``` + + + +## 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}" +``` diff --git a/develop.sh b/develop.sh new file mode 100755 index 0000000..434336f --- /dev/null +++ b/develop.sh @@ -0,0 +1,23 @@ +#!/bin/bash +fail() { + echo "$*" >&2 + exit 1 +} + +declare project=$1 + +if [[ "${project}" == "" ]]; then + echo "usage: $0 PROJECT" >&2 + exit 1 +fi + +declare full_path=$(realpath "projects/${project}") + +if ! docker compose run -it --rm \ + --name "${project}-seed" \ + -v "${full_path}:/projects/${project}" \ + android-dev-container -- \ + develop \ + "${project}"; then + fail "Unable to launch development container for ${project}" +fi diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..09b82b8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,40 @@ +services: + android-sdk-manager: + build: + context: . + tty: true + ports: + - '5037:5037' + volumes: + - ./AndroidSDK:/sdk:rw +# - ./AndroidSDK:/opt/android-sdk:rw + - ./scripts:/scripts:rw + healthcheck: + disable: true + + 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: \ No newline at end of file diff --git a/launch.sh b/launch.sh new file mode 100755 index 0000000..57584ef --- /dev/null +++ b/launch.sh @@ -0,0 +1,23 @@ +#!/bin/bash +fail() { + echo "$*" >&2 + exit 1 +} + +declare project=$1 + +if [[ "${project}" == "" ]]; then + echo "usage: $0 PROJECT" >&2 + exit 1 +fi + +declare full_path=$(realpath "projects/${project}") + +if ! docker compose run --rm \ + --name "${project}-seed" \ + -v "${full_path}:/projects/${project}" \ + android-dev-container -- \ + run \ + "${project}"; then + fail "Unable to start container for ${project}" +fi diff --git a/license_accepter.sh b/license_accepter.sh new file mode 100755 index 0000000..f4adf95 --- /dev/null +++ b/license_accepter.sh @@ -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 diff --git a/projects/.keep b/projects/.keep new file mode 100644 index 0000000..e69de29 diff --git a/repos/.keep b/repos/.keep new file mode 100644 index 0000000..e69de29 diff --git a/scripts/entrypoint b/scripts/entrypoint new file mode 100755 index 0000000..94760ad --- /dev/null +++ b/scripts/entrypoint @@ -0,0 +1,114 @@ +#!/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 +init) + if [[ ! -e "${ANDROID_HOME}"/license/android-sdk-license ]]; then + if ! cp -a "${ANDROID_HOME}"/. /sdk; then + fail "Unable to copy ${ANDROID_HOME} to /sdk" + fi + fi + echo "AndroidSDK transfered to host bind mount." + ;; + +seed-project) + seed-project "${options[@]}" + exit $? + ;; +develop) + project="${1}" + project_type=$(cat "/projects/${project}/project_type") + 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 + ;; +run) + project="${1}" + cd "/projects/${project}" + project_type=$(cat "/projects/${project}/project_type") + case "${project_type}" in + "expo") + echo "Starting web app in ${project}" + while true; do npm run web; sleep 5; done + ;; + "flutter") + echo "Launching terminal in ${project}" + /bin/bash + ;; + esac + ;; +*bash|shell) + /bin/bash "${options[@]}" + exit $? + ;; +*) + ${command} "${@}" + ;; +esac diff --git a/scripts/lib/common b/scripts/lib/common new file mode 100644 index 0000000..f547fdc --- /dev/null +++ b/scripts/lib/common @@ -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 "" +} diff --git a/scripts/seed-project b/scripts/seed-project new file mode 100755 index 0000000..45137ac --- /dev/null +++ b/scripts/seed-project @@ -0,0 +1,133 @@ +#!/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 + + echo "${project_type}" > project_type + ;; +"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 + cd "${project}" + if ! chown -R ${user}:${group} .; then + fail "chown -R ${user}:${group} ." + fi + echo "${project_type}" > project_type + ;; +esac + diff --git a/seed.sh b/seed.sh new file mode 100755 index 0000000..69d6234 --- /dev/null +++ b/seed.sh @@ -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: $0 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 diff --git a/shell.sh b/shell.sh new file mode 100755 index 0000000..c1341fa --- /dev/null +++ b/shell.sh @@ -0,0 +1,21 @@ +#!/bin/bash +fail() { + echo "$*" >&2 + exit 1 +} + +declare project=$1 + +if [[ "${project}" == "" ]]; then + echo "usage: ${0} PROJECT" >&2 + exit 1 +fi + +declare full_path=$(realpath "projects/${project}") + +if ! docker compose exec -it \ + "${project}" \ + shell; then + fail "Unable to launch shell in '${project}'. Make sure it is running via './launch.sh ${project}'." +fi +