├── MANIFEST.in ├── fetchcommandwrapper ├── __init__.py ├── version.py └── __main__.py ├── .gitignore ├── Makefile ├── .github ├── dependabot.yml └── workflows │ ├── dummy-portageq.sh │ ├── codespell.yml │ ├── pre-commit.yml │ ├── run-tests.yml │ └── pre-commit-detect-outdated.yml ├── ruff.toml ├── .pre-commit-config.yaml ├── make.conf ├── README.md └── setup.py /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include make.conf 2 | -------------------------------------------------------------------------------- /fetchcommandwrapper/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /MANIFEST 3 | *.pyc 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | dist: 2 | rm -f MANIFEST 3 | python3 setup.py sdist 4 | 5 | clean: 6 | find -type f -name '*.pyc' -delete 7 | 8 | .PHONY: clean dist 9 | -------------------------------------------------------------------------------- /fetchcommandwrapper/version.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010-2020 Sebastian Pipping 2 | # Licensed under GPL v3 or later 3 | 4 | VERSION = (1, 0, 0) 5 | VERSION_STR = ".".join(str(e) for e in VERSION) 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | commit-message: 6 | include: "scope" 7 | prefix: "Actions" 8 | directory: "/" 9 | labels: 10 | - "enhancement" 11 | schedule: 12 | interval: "weekly" 13 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | indent-width = 4 2 | line-length = 100 3 | target-version = "py310" 4 | 5 | [lint] 6 | select = [ 7 | "E", # pycodestyle 8 | "F", # Pyflakes + flake8 9 | "I", # isort 10 | "UP", # pyupgrade 11 | "W", # pycodestyle 12 | ] 13 | ignore = [ 14 | "UP031", # Use format specifiers instead of percent format; TODO drop 15 | ] 16 | -------------------------------------------------------------------------------- /.github/workflows/dummy-portageq.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | mirrors=( 3 | http://ftp.spline.inf.fu-berlin.de/mirrors/gentoo/ 4 | http://ftp-stud.hs-esslingen.de/pub/Mirrors/gentoo/ 5 | http://ftp.uni-erlangen.de/pub/mirrors/gentoo/ 6 | http://ftp.halifax.rwth-aachen.de/gentoo/ 7 | http://linux.rz.ruhr-uni-bochum.de/download/gentoo-mirror/ 8 | ftp://ftp.wh2.tu-dresden.de/pub/mirrors/gentoo/ 9 | ftp://sunsite.informatik.rwth-aachen.de/pub/Linux/gentoo/ 10 | ftp://ftp.tu-clausthal.de/pub/linux/gentoo/ 11 | ) 12 | echo "${mirrors[@]}" 13 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Sebastian Pipping 2 | # Licensed under GPL v3 or later 3 | 4 | repos: 5 | - repo: https://github.com/pre-commit/pre-commit-hooks 6 | rev: v6.0.0 7 | hooks: 8 | - id: check-merge-conflict 9 | - id: check-toml 10 | - id: check-yaml 11 | - id: end-of-file-fixer 12 | - id: trailing-whitespace 13 | 14 | - repo: https://github.com/astral-sh/ruff-pre-commit 15 | rev: v0.14.8 16 | hooks: 17 | - id: ruff 18 | args: 19 | - --fix 20 | - id: ruff-format 21 | -------------------------------------------------------------------------------- /make.conf: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010-2017 Sebastian Pipping 2 | # Licensed under GPL v3 or later 3 | 4 | ### Append "source /usr/share/fetchcommandwrapper/make.conf" 5 | ### to /etc/portage/make.conf to integrate fetchcommandwrapper. 6 | ### 7 | ### Specify additional parameters _before_ that using 8 | ### FETCHCOMMANDWRAPPER_OPTIONS in /etc/make.conf 9 | 10 | FETCHCOMMAND="/usr/bin/fetchcommandwrapper ${FETCHCOMMANDWRAPPER_OPTIONS} \${URI} \${DISTDIR} \${FILE} -- ${FETCHCOMMANDWRAPPER_EXTRA}" 11 | RESUMECOMMAND="/usr/bin/fetchcommandwrapper ${FETCHCOMMANDWRAPPER_OPTIONS} --continue \${URI} \${DISTDIR} \${FILE} -- ${FETCHCOMMANDWRAPPER_EXTRA}" 12 | -------------------------------------------------------------------------------- /.github/workflows/codespell.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Sebastian Pipping 2 | # Licensed under GPL v3 or later 3 | 4 | name: Enforce codespell-clean spelling 5 | 6 | on: 7 | pull_request: 8 | push: 9 | schedule: 10 | - cron: '0 2 * * 5' # Every Friday at 2am 11 | workflow_dispatch: 12 | 13 | # Drop permissions to minimum, for security 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | codespell: 19 | name: Enforce codespell-clean spelling 20 | runs-on: ubuntu-22.04 21 | steps: 22 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 23 | - uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # v2.2 24 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Sebastian Pipping 2 | # Licensed under GPL v3 or later 3 | 4 | name: Run pre-commit 5 | 6 | # Drop permissions to minimum, for security 7 | permissions: 8 | contents: read 9 | 10 | on: 11 | pull_request: 12 | push: 13 | schedule: 14 | - cron: '0 3 * * 5' # Every Friday at 3am 15 | workflow_dispatch: 16 | 17 | jobs: 18 | pre-commit: 19 | name: Run pre-commit 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 23 | - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 24 | with: 25 | python-version: 3.14 26 | - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | **fetchcommandwrapper** combines 4 | download tool [aria2](https://aria2.github.io/) 5 | with variable `GENTOO_MIRRORS` 6 | to speed up distfile downloads of [Portage](https://wiki.gentoo.org/wiki/Portage) 7 | by downloading from multiple mirrors at the same time. 8 | **fetchcommandwrapper** integrates with Portage 9 | through variables `FETCHCOMMAND` (and `RESUMECOMMAND`), hence the name. 10 | 11 | 12 | # Installation 13 | 14 | ## System-wide installation in Gentoo 15 | 16 | ```console 17 | # sudo emerge -av app-portage/fetchcommandwrapper 18 | ``` 19 | You can then append line `source /usr/share/fetchcommandwrapper/make.conf` 20 | to file `/etc/portage/make.conf` to ease integration with Portage. 21 | 22 | 23 | ## Development from a Git clone 24 | 25 | ```console 26 | # pip install --user --editable . 27 | ``` 28 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Sebastian Pipping 2 | # Licensed under GPL v3 or later 3 | 4 | name: Run the test suite 5 | 6 | # Drop permissions to minimum, for security 7 | permissions: 8 | contents: read 9 | 10 | on: 11 | pull_request: 12 | push: 13 | schedule: 14 | - cron: '0 3 * * 5' # Every Friday at 3am 15 | workflow_dispatch: 16 | 17 | jobs: 18 | run-tests: 19 | name: Run the test suite 20 | strategy: 21 | matrix: 22 | python-version: ["3.10", 3.14] # no particular need for in-between versions 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 26 | - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | - name: Install dependencies 30 | run: | 31 | sudo apt-get update 32 | sudo apt-get install --no-install-recommends --yes -V \ 33 | aria2 34 | 35 | - name: Install fetchcommandwrapper 36 | run: | 37 | set -x -u 38 | pip3 install -e . 39 | echo "${HOME}/.local/bin" >> "${GITHUB_PATH}" 40 | 41 | - name: Install dummy portageq 42 | run: | 43 | sudo cp -v .github/workflows/dummy-portageq.sh /usr/bin/portageq 44 | sudo chmod a+x /usr/bin/portageq 45 | 46 | - name: Run smoke tests 47 | run: | 48 | set -x 49 | 50 | python3 --version 51 | head -n1 "$(type -P fetchcommandwrapper)" 52 | 53 | fetchcommandwrapper --help 54 | fetchcommandwrapper --version 55 | 56 | args=( 57 | --link-speed 100000 58 | http://ftp.spline.inf.fu-berlin.de/mirrors/gentoo/distfiles/4f/isomaster-1.3.17.tar.bz2 59 | ./ 60 | isomaster-1.3.17.tar.bz2 61 | ) 62 | 63 | fetchcommandwrapper --fresh "${args[@]}" 64 | fetchcommandwrapper --continue "${args[@]}" 65 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (C) 2010 Sebastian Pipping 3 | # Licensed under GPL v3 or later 4 | 5 | import sys 6 | 7 | from setuptools import find_packages, setup 8 | 9 | sys.path.insert(0, "modules") 10 | from fetchcommandwrapper.version import VERSION_STR 11 | 12 | setup( 13 | name="fetchcommandwrapper", 14 | description="Wrapper around Aria2 for portage's FETCHCOMMAND variable", 15 | long_description=open("README.md").read(), 16 | long_description_content_type="text/markdown", 17 | license="GPL v3 or later", 18 | version=VERSION_STR, 19 | url="https://github.com/hartwork/fetchcommandwrapper", 20 | author="Sebastian Pipping", 21 | author_email="sping@gentoo.org", 22 | python_requires=">=3.10", 23 | setup_requires=[ 24 | "setuptools>=38.6.0", # for long_description_content_type 25 | ], 26 | packages=find_packages(), 27 | entry_points={ 28 | "console_scripts": [ 29 | "fetchcommandwrapper = fetchcommandwrapper.__main__:main", 30 | ], 31 | }, 32 | data_files=[ 33 | ( 34 | "share/fetchcommandwrapper", 35 | [ 36 | "make.conf", 37 | ], 38 | ), 39 | ], 40 | classifiers=[ 41 | "Development Status :: 4 - Beta", 42 | "Intended Audience :: End Users/Desktop", 43 | "Intended Audience :: System Administrators", 44 | "Programming Language :: Python", 45 | "Programming Language :: Python :: 3", 46 | "Programming Language :: Python :: 3.10", 47 | "Programming Language :: Python :: 3.11", 48 | "Programming Language :: Python :: 3.12", 49 | "Programming Language :: Python :: 3.13", 50 | "Programming Language :: Python :: 3.14", 51 | "Programming Language :: Python :: 3 :: Only", 52 | "Topic :: Internet", 53 | "Topic :: Internet :: File Transfer Protocol (FTP)", 54 | "Topic :: Internet :: WWW/HTTP", 55 | ], 56 | ) 57 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit-detect-outdated.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Sebastian Pipping 2 | # Licensed under GPL v3 or later 3 | 4 | name: Detect outdated pre-commit hooks 5 | 6 | on: 7 | schedule: 8 | - cron: '0 16 * * 5' # Every Friday 4pm 9 | workflow_dispatch: 10 | 11 | # NOTE: This will drop all permissions from GITHUB_TOKEN except metadata read, 12 | # and then (re)add the ones listed below: 13 | permissions: 14 | contents: write 15 | pull-requests: write 16 | 17 | jobs: 18 | pre_commit_detect_outdated: 19 | name: Detect outdated pre-commit hooks 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 23 | 24 | - name: Set up Python 3.14 25 | uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 26 | with: 27 | python-version: 3.14 28 | 29 | - name: Install pre-commit 30 | run: |- 31 | pip install \ 32 | --disable-pip-version-check \ 33 | --no-warn-script-location \ 34 | --user \ 35 | pre-commit 36 | echo "PATH=${HOME}/.local/bin:${PATH}" >> "${GITHUB_ENV}" 37 | 38 | - name: Check for outdated hooks 39 | run: |- 40 | pre-commit autoupdate 41 | git diff -- .pre-commit-config.yaml 42 | 43 | - name: Create pull request from changes (if any) 44 | id: create-pull-request 45 | uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 46 | with: 47 | author: 'pre-commit ' 48 | base: master 49 | body: |- 50 | For your consideration. 51 | 52 | :warning: Please **CLOSE AND RE-OPEN** this pull request so that [further workflow runs get triggered](https://github.com/peter-evans/create-pull-request/blob/main/docs/concepts-guidelines.md#triggering-further-workflow-runs) for this pull request. 53 | branch: precommit-autoupdate 54 | commit-message: "pre-commit: Autoupdate" 55 | delete-branch: true 56 | draft: true 57 | labels: enhancement 58 | title: "pre-commit: Autoupdate" 59 | 60 | - name: Log pull request URL 61 | if: "${{ steps.create-pull-request.outputs.pull-request-url }}" 62 | run: | 63 | echo "Pull request URL is: ${{ steps.create-pull-request.outputs.pull-request-url }}" 64 | -------------------------------------------------------------------------------- /fetchcommandwrapper/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (C) 2010-2017 Sebastian Pipping 3 | # Copyright (C) 2010 Andrew Karpow 4 | # Licensed under GPL v3 or later 5 | 6 | import os 7 | import sys 8 | from signal import SIGINT 9 | from textwrap import dedent 10 | 11 | MAX_STREAMS = 5 12 | ARIA2_COMMAND = "/usr/bin/aria2c" 13 | VERBOSE = os.getenv("PORTAGE_VERBOSE") == "1" 14 | 15 | 16 | def print_greeting(): 17 | from fetchcommandwrapper.version import VERSION_STR # noqa: I001 18 | 19 | print("fetchcommandwrapper %s" % VERSION_STR) 20 | print() 21 | 22 | 23 | def parse_parameters(): 24 | USAGE = "\n %(prog)s [OPTIONS] URI DISTDIR FILE [-- [ARG ..]]" 25 | EPILOG = dedent("""\ 26 | environment variables: 27 | NO_COLOR Disable use of color (default: auto-detect) 28 | PORTAGE_VERBOSE Enable verbose mode on "1" (default: low verbosity) 29 | """) 30 | 31 | import argparse # noqa: I001 32 | from fetchcommandwrapper.version import VERSION_STR # noqa: I001 33 | 34 | parser = argparse.ArgumentParser( 35 | prog="fetchcommandwrapper", 36 | usage=USAGE, 37 | epilog=EPILOG, 38 | formatter_class=argparse.RawDescriptionHelpFormatter, 39 | ) 40 | parser.add_argument("--version", action="version", version=VERSION_STR) 41 | 42 | group = parser.add_mutually_exclusive_group() 43 | group.add_argument( 44 | "-c", 45 | "--continue", 46 | action="store_true", 47 | dest="continue_flag", 48 | help="continue previous download", 49 | ) 50 | group.add_argument( 51 | "--fresh", 52 | action="store_false", 53 | dest="continue_flag", 54 | default=False, 55 | help="do not continue previous download (default)", 56 | ) 57 | parser.add_argument( 58 | "--link-speed", 59 | type=int, 60 | metavar="BYTES", 61 | dest="link_speed_bytes", 62 | help="specify link speed (bytes per second). enables dropping of slow connections.", 63 | ) 64 | parser.add_argument("uri", metavar="URI", help=argparse.SUPPRESS) 65 | parser.add_argument("distdir", metavar="DISTDIR", help=argparse.SUPPRESS) 66 | parser.add_argument("file_basename", metavar="FILE", help=argparse.SUPPRESS) 67 | 68 | opts, opts.argv_extra = parser.parse_known_args() 69 | 70 | opts.distdir = opts.distdir.rstrip("/") 71 | 72 | opts.file_fullpath = os.path.join(opts.distdir, opts.file_basename) 73 | 74 | return opts 75 | 76 | 77 | def invoke_wget(opts): 78 | args = ["/usr/bin/wget", "-O", opts.file_fullpath] 79 | args.append("--tries=5") 80 | args.append("--timeout=60") 81 | args.append("--passive-ftp") 82 | if opts.continue_flag: 83 | args.append("--continue") 84 | args.extend(opts.argv_extra) 85 | args.append(opts.uri) 86 | 87 | # Invoke wget 88 | print("Running... # %s" % " ".join(args)) 89 | import subprocess 90 | 91 | return subprocess.call(args) 92 | 93 | 94 | def print_invocation_details(opts): 95 | print("URI = %s" % opts.uri) 96 | print("DISTDIR = %s" % opts.distdir) 97 | print("FILE = %s" % opts.file_basename) 98 | print() 99 | 100 | 101 | def gentoo_mirrors(): 102 | import subprocess 103 | 104 | p = subprocess.Popen( 105 | ["/usr/bin/portageq", "gentoo_mirrors"], stdout=subprocess.PIPE, stderr=subprocess.PIPE 106 | ) 107 | (out, err) = p.communicate() 108 | if err: 109 | print("ERROR %s" % err.decode("UTF-8"), file=sys.stderr) 110 | sys.exit(1) 111 | return out.decode("UTF-8").rstrip("\n").split(" ") 112 | 113 | 114 | def supported(uri): 115 | return uri.startswith("http://") or uri.startswith("https://") or uri.startswith("ftp://") 116 | 117 | 118 | def print_mirror_details(supported_mirror_uris): 119 | if VERBOSE: 120 | print("Involved mirrors:") 121 | print("\n".join(" - " + e for e in supported_mirror_uris)) 122 | print(" (%d mirrors)" % len(supported_mirror_uris)) 123 | print() 124 | 125 | if len(supported_mirror_uris) < MAX_STREAMS: 126 | print( 127 | dedent( 128 | """\ 129 | WARNING: Please specify at least %d URIs in GENTOO_MIRRORS. 130 | The more the better.' 131 | """ 132 | % MAX_STREAMS 133 | ), 134 | file=sys.stderr, 135 | ) 136 | 137 | 138 | def make_final_uris(uri, supported_mirror_uris): 139 | final_uris = [ 140 | uri, 141 | ] 142 | mirrors_involved = False 143 | 144 | if not uri.endswith("/distfiles/layout.conf"): 145 | for i, mirror_uri in enumerate(supported_mirror_uris): 146 | if uri.startswith(mirror_uri): 147 | if i != 0: 148 | # Portage calls us for each mirror URI. Therefore we need 149 | # to error out on all but the first one, so we try each 150 | # mirrror once, at most. 151 | # This happens, when a file is not mirrored, e.g. with 152 | # sunrise ebuilds. 153 | print("ERROR: All Gentoo mirrors tried already, exiting.", file=sys.stderr) 154 | sys.exit(1) 155 | 156 | mirrors_involved = True 157 | 158 | local_part = uri[len(mirror_uri) :] 159 | final_uris = [e + local_part for e in supported_mirror_uris] 160 | import random 161 | 162 | random.shuffle(final_uris) 163 | break 164 | 165 | return final_uris, mirrors_involved 166 | 167 | 168 | def invoke_aria2(opts, final_uris): 169 | # Compose call arguments 170 | wanted_connections = min(MAX_STREAMS, len(final_uris)) 171 | if opts.link_speed_bytes and (len(final_uris) > MAX_STREAMS): 172 | drop_slow_links = True 173 | else: 174 | drop_slow_links = False 175 | 176 | if len(final_uris) > 1 and VERBOSE: 177 | print( 178 | "Targeting %d random connections, additional %d for backup" 179 | % (wanted_connections, max(0, len(final_uris) - MAX_STREAMS)) 180 | ) 181 | print() 182 | 183 | args = [ARIA2_COMMAND, "-d", opts.distdir, "-o", opts.file_basename] 184 | if drop_slow_links: 185 | wanted_minimum_link_speed = int(opts.link_speed_bytes / wanted_connections / 3) 186 | args.append("--lowest-speed-limit=%s" % wanted_minimum_link_speed) 187 | if opts.continue_flag: 188 | args.append("--continue") 189 | if not VERBOSE: 190 | args.append("--console-log-level=warn") 191 | args.append("--summary-interval=0") 192 | if os.getenv("NO_COLOR"): 193 | args.append("--enable-color=false") 194 | args.append("--allow-overwrite=true") 195 | args.append("--max-tries=5") 196 | args.append("--max-file-not-found=2") 197 | args.append("--user-agent=Wget/1.19.1") 198 | args.append("--split=%d" % wanted_connections) 199 | args.append("--max-connection-per-server=1") 200 | args.append("--uri-selector=inorder") 201 | args.append("--follow-metalink=mem") # e.g. GNOME servers with mirrorbrain 202 | args.extend(opts.argv_extra) 203 | args.extend(final_uris) 204 | 205 | # Invoke aria2 206 | if VERBOSE: 207 | print("Running... # %s" % " ".join(args)) 208 | import subprocess 209 | 210 | return subprocess.call(args) 211 | 212 | 213 | def _inner_main(): 214 | opts = parse_parameters() 215 | 216 | if not os.path.exists(ARIA2_COMMAND): 217 | print("ERROR: net-misc/aria2 not installed, falling back to net-misc/wget", file=sys.stderr) 218 | ret = invoke_wget(opts) 219 | sys.exit(ret) 220 | 221 | if not os.path.isdir(opts.distdir): 222 | print('ERROR: Path "%s" not a directory' % opts.distdir, file=sys.stderr) 223 | sys.exit(1) 224 | 225 | if opts.continue_flag: 226 | if not os.path.isfile(opts.file_fullpath): 227 | print('ERROR: Path "%s" not an existing file' % opts.file_fullpath, file=sys.stderr) 228 | sys.exit(1) 229 | 230 | supported_mirror_uris = [e for e in gentoo_mirrors() if supported(e)] 231 | 232 | final_uris, mirrors_involved = make_final_uris(opts.uri, supported_mirror_uris) 233 | 234 | if VERBOSE: 235 | print_greeting() 236 | print_invocation_details(opts) 237 | 238 | if mirrors_involved: 239 | print_mirror_details(supported_mirror_uris) 240 | 241 | ret = invoke_aria2(opts, final_uris) 242 | sys.exit(ret) 243 | 244 | 245 | def main(): 246 | try: 247 | _inner_main() 248 | except KeyboardInterrupt: 249 | sys.exit(128 + SIGINT) 250 | 251 | 252 | if __name__ == "__main__": 253 | main() 254 | --------------------------------------------------------------------------------