Browse Source

qt6: update recipe pins, NDK, SDK, Ant, use venv for buildozer/p4a, add tomli recipe

master
Sander van Grieken 2 years ago
parent
commit
770a32cf6a
  1. 53
      contrib/android/Dockerfile
  2. 1
      contrib/android/build.sh
  3. 12
      contrib/android/buildozer_qml.spec
  4. 10
      contrib/android/make_apk.sh
  5. 4
      contrib/android/p4a_recipes/Pillow/__init__.py
  6. 4
      contrib/android/p4a_recipes/libffi/__init__.py
  7. 4
      contrib/android/p4a_recipes/libiconv/__init__.py
  8. 6
      contrib/android/p4a_recipes/pyjnius/__init__.py
  9. 18
      contrib/android/p4a_recipes/pyqt5/__init__.py
  10. 18
      contrib/android/p4a_recipes/pyqt5sip/__init__.py
  11. 18
      contrib/android/p4a_recipes/pyqt6/__init__.py
  12. 18
      contrib/android/p4a_recipes/pyqt6sip/__init__.py
  13. 4
      contrib/android/p4a_recipes/pyqt_builder/__init__.py
  14. 16
      contrib/android/p4a_recipes/qt5/__init__.py
  15. 17
      contrib/android/p4a_recipes/qt6/__init__.py
  16. 6
      contrib/android/p4a_recipes/sip/__init__.py
  17. 4
      contrib/android/p4a_recipes/sqlite3/__init__.py
  18. 13
      contrib/android/p4a_recipes/tomli/__init__.py

53
contrib/android/Dockerfile

@ -31,12 +31,12 @@ RUN apt -y update -qq \
ENV ANDROID_NDK_HOME="${ANDROID_HOME}/android-ndk"
ENV ANDROID_NDK_VERSION="22b"
ENV ANDROID_NDK_HASH="ac3a0421e76f71dd330d0cd55f9d99b9ac864c4c034fc67e0d671d022d4e806b"
ENV ANDROID_NDK_VERSION="23b"
ENV ANDROID_NDK_HASH="c6e97f9c8cfe5b7be0a9e6c15af8e7a179475b7ded23e2d1c1fa0945d6fb4382"
ENV ANDROID_NDK_HOME_V="${ANDROID_NDK_HOME}-r${ANDROID_NDK_VERSION}"
# get the latest version from https://developer.android.com/ndk/downloads/index.html
ENV ANDROID_NDK_ARCHIVE="android-ndk-r${ANDROID_NDK_VERSION}-linux-x86_64.zip"
ENV ANDROID_NDK_ARCHIVE="android-ndk-r${ANDROID_NDK_VERSION}-linux.zip"
ENV ANDROID_NDK_DL_URL="https://dl.google.com/android/repository/${ANDROID_NDK_ARCHIVE}"
# download and install Android NDK
@ -53,9 +53,8 @@ RUN curl --location --progress-bar \
ENV ANDROID_SDK_HOME="${ANDROID_HOME}/android-sdk"
# get the latest version from https://developer.android.com/studio/index.html
ENV ANDROID_SDK_TOOLS_VERSION="8092744"
ENV ANDROID_SDK_BUILD_TOOLS_VERSION="30.0.3"
ENV ANDROID_SDK_HASH="d71f75333d79c9c6ef5c39d3456c6c58c613de30e6a751ea0dbd433e8f8b9cbf"
ENV ANDROID_SDK_TOOLS_VERSION="9477386"
ENV ANDROID_SDK_HASH="bd1aa17c7ef10066949c88dc6c9c8d536be27f992a1f3b5a584f9bd2ba5646a0"
ENV ANDROID_SDK_TOOLS_ARCHIVE="commandlinetools-linux-${ANDROID_SDK_TOOLS_VERSION}_latest.zip"
ENV ANDROID_SDK_TOOLS_DL_URL="https://dl.google.com/android/repository/${ANDROID_SDK_TOOLS_ARCHIVE}"
ENV ANDROID_SDK_MANAGER="${ANDROID_SDK_HOME}/cmdline-tools/bin/sdkmanager --sdk_root=${ANDROID_SDK_HOME}"
@ -77,19 +76,22 @@ RUN mkdir --parents "${ANDROID_SDK_HOME}/.android/" \
# accept Android licenses (JDK necessary!)
RUN apt -y update -qq \
&& apt -y install -qq --no-install-recommends --allow-downgrades \
openjdk-11-jdk-headless \
openjdk-17-jdk-headless \
&& apt -y autoremove
RUN yes | ${ANDROID_SDK_MANAGER} --licenses > /dev/null
ENV ANDROID_SDK_BUILD_TOOLS_VERSION="31.0.0"
# download platforms, API, build tools
RUN ${ANDROID_SDK_MANAGER} "platforms;android-30" > /dev/null && \
RUN ${ANDROID_SDK_MANAGER} "platforms;android-31" > /dev/null && \
${ANDROID_SDK_MANAGER} "build-tools;${ANDROID_SDK_BUILD_TOOLS_VERSION}" > /dev/null && \
${ANDROID_SDK_MANAGER} "extras;android;m2repository" > /dev/null && \
chmod +x "${ANDROID_SDK_HOME}/cmdline-tools/bin/avdmanager"
# download ANT
ENV APACHE_ANT_VERSION="1.9.4"
ENV APACHE_ANT_HASH="66d3edcbb0eba11387705cd89178ffb65e55cd53f13ca35c1bb983c0f9992540"
ENV APACHE_ANT_VERSION="1.10.13"
ENV APACHE_ANT_HASH="776be4a5704158f00ef3f23c0327546e38159389bc8f39abbfe114913f88bab1"
ENV APACHE_ANT_ARCHIVE="apache-ant-${APACHE_ANT_VERSION}-bin.tar.gz"
ENV APACHE_ANT_DL_URL="https://archive.apache.org/dist/ant/binaries/${APACHE_ANT_ARCHIVE}"
ENV APACHE_ANT_HOME="${ANDROID_HOME}/apache-ant"
@ -139,6 +141,15 @@ RUN apt -y update -qq \
&& apt -y autoremove \
&& apt -y clean
# cross compile deps for Qt6
RUN apt -y update -qq \
&& apt -y install -qq --no-install-recommends --allow-downgrades \
libopengl-dev \
libegl-dev \
dos2unix \
&& apt -y autoremove \
&& apt -y clean
# create new user to avoid using root; but with sudo access and no password for convenience.
ARG UID=1000
@ -154,12 +165,16 @@ RUN chown --recursive ${USER} ${WORK_DIR} ${ANDROID_SDK_HOME}
RUN chown ${USER} /opt
USER ${USER}
# venv, VIRTUAL_ENV is used by buildozer to indicate a venv environemnt
ENV VIRTUAL_ENV=/opt/venv
RUN python3 -m venv ${VIRTUAL_ENV}
ENV PATH="${VIRTUAL_ENV}/bin:${PATH}"
COPY contrib/deterministic-build/requirements-build-base.txt /opt/deterministic-build/
COPY contrib/deterministic-build/requirements-build-android.txt /opt/deterministic-build/
RUN python3 -m pip install --no-build-isolation --no-dependencies --user \
RUN /opt/venv/bin/python3 -m pip install --no-build-isolation --no-dependencies \
-r /opt/deterministic-build/requirements-build-base.txt
RUN python3 -m pip install --no-build-isolation --no-dependencies --no-binary :all: --user \
RUN /opt/venv/bin/python3 -m pip install --no-build-isolation --no-dependencies --no-binary :all: \
-r /opt/deterministic-build/requirements-build-android.txt
# install buildozer
@ -168,9 +183,10 @@ RUN cd /opt \
&& cd buildozer \
&& git remote add sombernight https://github.com/SomberNight/buildozer \
&& git fetch --all \
# commit: from branch sombernight/electrum_20210421 (note: careful with force-pushing! see #8162)
&& git checkout "6f03256e8312f8d1e5a6da3a2a1bf06e2738325e^{commit}" \
&& python3 -m pip install --no-build-isolation --no-dependencies --user -e .
# commit: from branch sombernight/electrum_20210421 (note: careful with force-pushing! see #8162) \
# no, this is master
&& git checkout "10f9c8b789f4f4cb020356bdb50607eceae10493^{commit}" \
&& /opt/venv/bin/python3 -m pip install --no-build-isolation --no-dependencies -e .
# install python-for-android
RUN cd /opt \
@ -179,9 +195,10 @@ RUN cd /opt \
&& git remote add sombernight https://github.com/SomberNight/python-for-android \
&& git remote add accumulator https://github.com/accumulator/python-for-android \
&& git fetch --all \
# commit: from branch accumulator/electrum_20210421d (note: careful with force-pushing! see #8162)
&& git checkout "052b9f7945bae557347fa4a4b418040d9da9eaff^{commit}" \
&& python3 -m pip install --no-build-isolation --no-dependencies --user -e .
# commit: from branch accumulator/electrum_20210421d (note: careful with force-pushing! see #8162) \
#
&& git checkout "710cc81d9cdcdcef910547074f031e8a3f102d63^{commit}" \
&& /opt/venv/bin/python3 -m pip install --no-build-isolation --no-dependencies -e .
# build env vars
ENV USE_SDK_WRAPPER=1

1
contrib/android/build.sh

@ -48,7 +48,6 @@ docker build \
--file "$CONTRIB_ANDROID/Dockerfile" \
"$PROJECT_ROOT"
# maybe do fresh clone
if [ ! -z "$ELECBUILD_COMMIT" ] ; then
info "ELECBUILD_COMMIT=$ELECBUILD_COMMIT. doing fresh clone and git checkout."

12
contrib/android/buildozer_qml.spec

@ -55,8 +55,8 @@ requirements =
libffi,
libsecp256k1,
cryptography,
pyqt5sip,
pyqt5,
pyqt6sip,
pyqt6,
pillow,
libzbar
@ -84,7 +84,7 @@ android.permissions = INTERNET, CAMERA, WRITE_EXTERNAL_STORAGE
# (int) Android API to use (compileSdkVersion)
# note: when changing, Dockerfile also needs to be changed to install corresponding build tools
android.api = 30
android.api = 31
# (int) Android targetSdkVersion
android.target_sdk_version = 31
@ -93,13 +93,13 @@ android.target_sdk_version = 31
android.minapi = 21
# (str) Android NDK version to use
android.ndk = 22b
android.ndk = 23b
# (int) Android NDK API to use (optional). This is the minimum API your app will support.
android.ndk_api = 21
# (bool) Use --private data storage (True) or --dir public storage (False)
android.private_storage = True
#android.private_storage = True
# (str) Android NDK directory (if empty, it will be automatically downloaded.)
android.ndk_path = /opt/android/android-ndk
@ -199,7 +199,7 @@ p4a.local_recipes = %(source.dir)s/contrib/android/p4a_recipes/
#p4a.hook =
# (str) Bootstrap to use for android builds
p4a.bootstrap = qt5
p4a.bootstrap = qt6
# (int) port number to specify an explicit --port= p4a argument (eg for bootstrap flask)
#p4a.port =

10
contrib/android/make_apk.sh

@ -90,16 +90,16 @@ fi
if [[ "$2" == "all" ]] ; then
# build all apks
# FIXME failures are not propagated out: we should fail the script if any arch build fails
export APP_ANDROID_ARCH=armeabi-v7a
export APP_ANDROID_ARCHS=armeabi-v7a
make $TARGET
export APP_ANDROID_ARCH=arm64-v8a
export APP_ANDROID_ARCHS=arm64-v8a
make $TARGET
#export APP_ANDROID_ARCH=x86
#export APP_ANDROID_ARCHS=x86
#make $TARGET
export APP_ANDROID_ARCH=x86_64
export APP_ANDROID_ARCHS=x86_64
make $TARGET
else
export APP_ANDROID_ARCH=$2
export APP_ANDROID_ARCHS=$2
make $TARGET
fi

4
contrib/android/p4a_recipes/Pillow/__init__.py

@ -6,13 +6,13 @@ from pythonforandroid.util import load_source
util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py'))
assert PillowRecipe._version == "7.0.0"
assert PillowRecipe._version == "8.4.0"
assert PillowRecipe.depends == ['png', 'jpeg', 'freetype', 'setuptools', 'python3']
assert PillowRecipe.python_depends == []
class PillowRecipePinned(util.InheritedRecipeMixin, PillowRecipe):
sha512sum = "187173a525d4f3f01b4898633263b53a311f337aa7b159c64f79ba8c7006fd44798a058e7cc5d8f1116bad008e4142ff303456692329fe73b0e115ef5c225d73"
sha512sum = "d395f69ccb37c52a3b6f45836700ffbc3173afae31848cc61d7b47db88ca1594541023beb9a14fd9067aca664e182c7d6e3300ab3e3095c31afe8dcbc6e08233"
recipe = PillowRecipePinned()

4
contrib/android/p4a_recipes/libffi/__init__.py

@ -6,13 +6,13 @@ from pythonforandroid.util import load_source
util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py'))
assert LibffiRecipe._version == "v3.3"
assert LibffiRecipe._version == "v3.4.2"
assert LibffiRecipe.depends == []
assert LibffiRecipe.python_depends == []
class LibffiRecipePinned(util.InheritedRecipeMixin, LibffiRecipe):
sha512sum = "62798fb31ba65fa2a0e1f71dd3daca30edcf745dc562c6f8e7126e54db92572cc63f5aa36d927dd08375bb6f38a2380ebe6c5735f35990681878fc78fc9dbc83"
sha512sum = "d399319efcca375fe901b05722e25eca31d11a4261c6a5d5079480bbc552d4e4b42de2026912689d3b2f886ebb3c8bebbea47102e38a2f6acbc526b8d5bba388"
recipe = LibffiRecipePinned()

4
contrib/android/p4a_recipes/libiconv/__init__.py

@ -6,13 +6,13 @@ from pythonforandroid.util import load_source
util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py'))
assert LibIconvRecipe._version == "1.15"
assert LibIconvRecipe._version == "1.16"
assert LibIconvRecipe.depends == []
assert LibIconvRecipe.python_depends == []
class LibIconvRecipePinned(util.InheritedRecipeMixin, LibIconvRecipe):
sha512sum = "1233fe3ca09341b53354fd4bfe342a7589181145a1232c9919583a8c9979636855839049f3406f253a9d9829908816bb71fd6d34dd544ba290d6f04251376b1a"
sha512sum = "365dac0b34b4255a0066e8033a8b3db4bdb94b9b57a9dca17ebf2d779139fe935caf51a465d17fd8ae229ec4b926f3f7025264f37243432075e5583925bb77b7"
recipe = LibIconvRecipePinned()

6
contrib/android/p4a_recipes/pyjnius/__init__.py

@ -6,13 +6,13 @@ from pythonforandroid.util import load_source
util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py'))
assert PyjniusRecipe._version == "1.3.0"
assert PyjniusRecipe.depends == [('genericndkbuild', 'sdl2', 'qt5'), 'six', 'python3']
assert PyjniusRecipe._version == "1.5.0"
assert PyjniusRecipe.depends == [('genericndkbuild', 'sdl2', 'qt6'), 'six', 'python3']
assert PyjniusRecipe.python_depends == []
class PyjniusRecipePinned(util.InheritedRecipeMixin, PyjniusRecipe):
sha512sum = "5a3475afcda5afbef6e1a67bab508e3c24bd564efda5ac38ae7669d39b4bfdbfaaa83f435f26d39b3d849d3a167a9c136c9ac6b2bfcc0bda09ef1c00aa66cf25"
sha512sum = "e47ff08bdcda8fc9ef9617fc84515a85404d77cfce3ede3e190ae21221837a4275840e14976271f38eb5d514682d22eab5d83d8ca94dbf3a6b47d4effa109790"
recipe = PyjniusRecipePinned()

18
contrib/android/p4a_recipes/pyqt5/__init__.py

@ -1,18 +0,0 @@
import os
from pythonforandroid.recipes.pyqt5 import PyQt5Recipe
from pythonforandroid.util import load_source
util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py'))
assert PyQt5Recipe._version == "5.15.9"
assert PyQt5Recipe.depends == ['qt5', 'pyjnius', 'setuptools', 'pyqt5sip', 'hostpython3', 'pyqt_builder']
assert PyQt5Recipe.python_depends == []
class PyQt5RecipePinned(util.InheritedRecipeMixin, PyQt5Recipe):
sha512sum = "1c07d93aefe1c24e80851eb4631b80a99e7ba06e823181325456edb90285d3d22417a9f7d4c3ff9c6195bd801e7dc2bbabf0587af844a5e4b0a410c4611d119e"
recipe = PyQt5RecipePinned()

18
contrib/android/p4a_recipes/pyqt5sip/__init__.py

@ -1,18 +0,0 @@
import os
from pythonforandroid.recipes.pyqt5sip import PyQt5SipRecipe
from pythonforandroid.util import load_source
util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py'))
assert PyQt5SipRecipe._version == "12.11.1"
assert PyQt5SipRecipe.depends == ['setuptools', 'python3']
assert PyQt5SipRecipe.python_depends == []
class PyQt5SipRecipePinned(util.InheritedRecipeMixin, PyQt5SipRecipe):
sha512sum = "9a24b6e8356fdb1070672ee37e5f4259d72a75bb60376ad0946274331ae29a6cceb98a6c5a278bf5e8015a3d493c925bacab8593ef02c310ff3773bd3ee46a5d"
recipe = PyQt5SipRecipePinned()

18
contrib/android/p4a_recipes/pyqt6/__init__.py

@ -0,0 +1,18 @@
import os
from pythonforandroid.recipes.pyqt6 import PyQt6Recipe
from pythonforandroid.util import load_source
util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py'))
assert PyQt6Recipe._version == "6.4.2"
assert PyQt6Recipe.depends == ['qt6', 'pyjnius', 'setuptools', 'pyqt6sip', 'hostpython3', 'pyqt_builder']
assert PyQt6Recipe.python_depends == []
class PyQt6RecipePinned(util.InheritedRecipeMixin, PyQt6Recipe):
sha512sum = "51e5f0d028ee7984876da1653cb135d61e2c402f18b939a92477888cc7c86d3bc2889477403dee6b3d9f66519ee3236d344323493b4c2c2e658e1637b10e53bf"
recipe = PyQt6RecipePinned()

18
contrib/android/p4a_recipes/pyqt6sip/__init__.py

@ -0,0 +1,18 @@
import os
from pythonforandroid.recipes.pyqt6sip import PyQt6SipRecipe
from pythonforandroid.util import load_source
util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py'))
assert PyQt6SipRecipe._version == "13.5.1"
assert PyQt6SipRecipe.depends == ['setuptools', 'python3']
assert PyQt6SipRecipe.python_depends == []
class PyQt6SipRecipePinned(util.InheritedRecipeMixin, PyQt6SipRecipe):
sha512sum = "1e4170d167a326afe6df86e4a35e209299548054981cb2e5d56da234ef9db4d8594bcb05b6be363c3bc6252776ae9de63d589a3d9f33fba8250d39cdb5e9061a"
recipe = PyQt6SipRecipePinned()

4
contrib/android/p4a_recipes/pyqt_builder/__init__.py

@ -1,13 +1,13 @@
from pythonforandroid.recipes.pyqt_builder import PyQtBuilderRecipe
assert PyQtBuilderRecipe._version == "1.14.1"
assert PyQtBuilderRecipe._version == "1.15.1"
assert PyQtBuilderRecipe.depends == ["sip", "packaging", "python3"]
assert PyQtBuilderRecipe.python_depends == []
class PyQtBuilderRecipePinned(PyQtBuilderRecipe):
sha512sum = "4de9be2c42f38fbc22d46a31dd6da37c02620bb112a674ef846a4eb7f862715852e1d7328da1e0d0e33f78475166fe3c690e710e18bfeb48f840f137831a2182"
sha512sum = "61ee73b6bb922c04739da60025ab50d35d345d2e298943305fcbd3926cda31d732cc5e5b0dbfc39f5eb85c0f0b091b8c3f5fee00dcc240d7849c5c4191c1368a"
recipe = PyQtBuilderRecipePinned()

16
contrib/android/p4a_recipes/qt5/__init__.py

@ -1,16 +0,0 @@
import os
from pythonforandroid.recipes.qt5 import Qt5Recipe
from pythonforandroid.util import load_source
util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py'))
assert Qt5Recipe._version == "95254e52c658729e80f741324045034c15ce9cb0"
assert Qt5Recipe.depends == ['python3']
assert Qt5Recipe.python_depends == []
class Qt5RecipePinned(util.InheritedRecipeMixin, Qt5Recipe):
pass
recipe = Qt5RecipePinned()

17
contrib/android/p4a_recipes/qt6/__init__.py

@ -0,0 +1,17 @@
import os
from pythonforandroid.recipes.qt6 import Qt6Recipe
from pythonforandroid.util import load_source
util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py'))
assert Qt6Recipe._version == "6.4.3"
assert Qt6Recipe.depends == ['python3', 'hostqt6']
assert Qt6Recipe.python_depends == []
class Qt6RecipePinned(util.InheritedRecipeMixin, Qt6Recipe):
sha512sum = "767d2d388dab64ba314743841b9b2dbd68996876d15621e0ae97688e2ef1300c70f96b417bf111f119c87699a3d7014c70aec3a80b5216212bb5d35979230db7"
recipe = Qt6RecipePinned()

6
contrib/android/p4a_recipes/sip/__init__.py

@ -1,13 +1,13 @@
from pythonforandroid.recipes.sip import SipRecipe
assert SipRecipe._version == "6.7.7"
assert SipRecipe.depends == ["setuptools", "packaging", "toml", "ply", "python3"], SipRecipe.depends
assert SipRecipe._version == "6.7.9"
assert SipRecipe.depends == ["setuptools", "packaging", "tomli", "ply", "python3"], SipRecipe.depends
assert SipRecipe.python_depends == []
class SipRecipePinned(SipRecipe):
sha512sum = "b41a1e53e8bad1fca08eda2c89b8a7cabe6cb9e54d0ddeba0c718499b0288633fb6b90128d54f3df2420e20bb217d3df224750d30e865487d2b0a640fba82444"
sha512sum = "bb9d0d0d92002b6fd33f7e8ebe8cd62456dacc16b5734b73760b1ba14fb9b1f2b9b6640b40196c6cf5f345e1afde48bdef39675c4d3480041771325d4cf3c233"
recipe = SipRecipePinned()

4
contrib/android/p4a_recipes/sqlite3/__init__.py

@ -6,13 +6,13 @@ from pythonforandroid.util import load_source
util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py'))
assert Sqlite3Recipe._version == "3.34.1"
assert Sqlite3Recipe._version == "3.35.5"
assert Sqlite3Recipe.depends == []
assert Sqlite3Recipe.python_depends == []
class Sqlite3RecipePinned(util.InheritedRecipeMixin, Sqlite3Recipe):
sha512sum = "8a936f1c34fc9036cadf5bd53f9ee594135c2efdef1d2c82bd4fdf3e0218afde710fc4c436cfc992687d008e6086a697da0487352ed88809d677e05d824940dd"
sha512sum = "9684fee89224f0c975c280cb6b2c64adb040334bc5517dfe0e354b0557459fa3ae642c4289a7a5265f65b3ad5b6747db8068a1e5172fbb8edec7f6d964ecbb20"
recipe = Sqlite3RecipePinned()

13
contrib/android/p4a_recipes/tomli/__init__.py

@ -0,0 +1,13 @@
from pythonforandroid.recipes.tomli import TomliRecipe
assert TomliRecipe._version == "2.0.1"
assert TomliRecipe.depends == ["setuptools", "python3"]
assert TomliRecipe.python_depends == []
class TomliRecipePinned(TomliRecipe):
sha512sum = "fd410039e255e2b3359e999d69a5a2d38b9b89b77e8557f734f2621dfbd5e1207e13aecc11589197ec22594c022f07f41b4cfe486a3a719281a595c95fd19ecf"
recipe = TomliRecipePinned()
Loading…
Cancel
Save