diff --git a/.gitignore b/.gitignore index 516d3d3..243c79d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ +venv/ *.pyc __pycache__/ dist/ build/ -AutoROM.egg-info/ \ No newline at end of file +*.egg-info/ diff --git a/README.md b/README.md index 851155f..9419ebd 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,10 @@ AutoROM ``` > IMPORTANT: We do not have official support for Windows. However, if you are on Windows and encounter a DLL import error, you may need to install [OpenSSL v1.1.1S](https://slproweb.com/products/Win32OpenSSL.html). -> IMPORTANT: For now, an alternative solution to the above known issues is to use AutoROM v0.4.2, which uses a different source for where the ROMs are sourced from. - +If you encounter problems with installation due to your network blocking torrenting, it is possible to install the ROMs using a pre-downloaded roms.tar.gz file. +``` +AutoROM --source-file /path/to/roms.tar.gz +``` To specify a specific installation directory for your ROMs, use the `--install-dir` command line flag. ``` AutoROM --install-dir /path/to/install diff --git a/packages/AutoROM.accept-rom-license/setup.py b/packages/AutoROM.accept-rom-license/setup.py index de88029..b6b5806 100644 --- a/packages/AutoROM.accept-rom-license/setup.py +++ b/packages/AutoROM.accept-rom-license/setup.py @@ -8,11 +8,11 @@ class InstallCommand(install): def run(self): super().run() - from AutoROM import main as AutoROM + from AutoROM import main download_dir = pathlib.Path(self.install_lib) / "AutoROM" / "roms" - download_dir.mkdir(exist_ok=False, parents=True) - AutoROM(True, download_dir, False) + download_dir.mkdir(exist_ok=True, parents=True) + main(True, None, download_dir, False) setuptools.setup( diff --git a/scripts/ci-test.sh b/scripts/ci-test.sh index 24b7fb3..de5e525 100755 --- a/scripts/ci-test.sh +++ b/scripts/ci-test.sh @@ -19,35 +19,41 @@ test_cleanup() { # Test procedure test_autorom() { set -e + # Get install flag local install_to_pkgs=false - while getopts 'i' opt; do + local pretorrented=false + while getopts 'ip' opt; do case $opt in i) install_to_pkgs=true ;; + p) pretorrented=true ;; esac done - # Work in roms directory + # work in new directory mkdir -p roms && pushd roms - # Install locally - AutoROM --accept-license --install-dir . && ls -l - - # Conditionally install to packages if [ "$install_to_pkgs" = true ]; then + # conditionally install to packages AutoROM --accept-license + elif [ "$pretorrented" = true ]; then + # conditionally test using pretorrented + AutoROM --accept-license --source-file ../scripts/Roms.tar.gz + else + # generic install + AutoROM --accept-license --install-dir . fi # Print ROMs the ALE can find python -c "import ale_py.roms as roms; print(roms.__all__)" - # Cleanup + # cleanup popd && rm -r roms } ./scripts/build-dist.sh -# Test local pip insall with installing to packages +# Test local pip install with installing to packages echo "::group::Test AutoROM CLI install" test_init @@ -74,3 +80,13 @@ pip install --find-links dist/ --no-cache-dir AutoROM[accept-rom-license] test_autorom test_cleanup echo "::endgroup::" + +# Test installing using pre-torrented tar +echo "::group::Test AutoROM[accept-rom-license]" +test_init +pip install --find-links dist/ --no-cache-dir AutoROM +python ./scripts/torrent_tar.py + +test_autorom -p +test_cleanup +echo "::endgroup::" diff --git a/scripts/torrent_tar.py b/scripts/torrent_tar.py new file mode 100644 index 0000000..6dfbac3 --- /dev/null +++ b/scripts/torrent_tar.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +import os +import sys +import time + +import libtorrent as lt + + +def torrent_tar(): + # specify the save path + save_path = os.path.dirname(__file__) + + # magnet uri + uri = "magnet:?xt=urn:btih:a606d1dabf28e794cbc0f88f10d0b8225dc854b4&dn=Roms.tar.gz&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=http%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=udp%3A%2F%2F9.rarbg.com%3A2810%2Fannounce&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A6969%2Fannounce&tr=http%3A%2F%2Ftracker.openbittorrent.com%3A80%2Fannounce&tr=udp%3A%2F%2Fopentracker.i2p.rocks%3A6969%2Fannounce&tr=https%3A%2F%2Fopentracker.i2p.rocks%3A443%2Fannounce&tr=udp%3A%2F%2Fopen.stealth.si%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.torrent.eu.org%3A451%2Fannounce&tr=udp%3A%2F%2Ftracker2.dler.org%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.tiny-vps.com%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.moeking.me%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.dler.org%3A6969%2Fannounce&tr=udp%3A%2F%2Fpublic.tracker.vraphim.com%3A6969%2Fannounce&tr=udp%3A%2F%2Fp4p.arenabg.com%3A1337%2Fannounce&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce&tr=udp%3A%2F%2Fmovies.zsw.ca%3A6969%2Fannounce&tr=udp%3A%2F%2Fipv4.tracker.harry.lu%3A80%2Fannounce&tr=udp%3A%2F%2Ffe.dealclub.de%3A6969%2Fannounce&tr=udp%3A%2F%2Fexplodie.org%3A6969%2Fannounce&tr=udp%3A%2F%2Fexodus.desync.com%3A6969%2Fannounce&tr=udp%3A%2F%2Fbt2.archive.org%3A6969%2Fannounce&tr=udp%3A%2F%2Fbt1.archive.org%3A6969%2Fannounce&tr=udp%3A%2F%2F6ahddutb1ucc3cp.ru%3A6969%2Fannounce&tr=https%3A%2F%2Ftracker.nanoha.org%3A443%2Fannounce&tr=https%3A%2F%2Ftracker.lilithraws.org%3A443%2Fannounce&tr=https%3A%2F%2Ftr.burnabyhighstar.com%3A443%2Fannounce&tr=http%3A%2F%2Fvps02.net.orel.ru%3A80%2Fannounce&tr=http%3A%2F%2Ftracker2.dler.org%3A80%2Fannounce&tr=http%3A%2F%2Ftracker.mywaifu.best%3A6969%2Fannounce&tr=http%3A%2F%2Ftracker.files.fm%3A6969%2Fannounce&tr=http%3A%2F%2Ftracker.dler.org%3A6969%2Fannounce&tr=http%3A%2F%2Ft.overflow.biz%3A6969%2Fannounce&tr=udp%3A%2F%2Fzecircle.xyz%3A6969%2Fannounce&tr=udp%3A%2F%2Fyahor.ftp.sh%3A6969%2Fannounce&tr=udp%3A%2F%2Fvibe.sleepyinternetfun.xyz%3A1738%2Fannounce&tr=udp%3A%2F%2Fuploads.gamecoast.net%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker1.bt.moack.co.kr%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.theoks.net%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.tcp.exchange%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.swateam.org.uk%3A2710%2Fannounce&tr=udp%3A%2F%2Ftracker.srv00.com%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.skyts.net%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.publictracker.xyz%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.pomf.se%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.openbtba.com%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.monitorit4.me%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.lelux.fi%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.leech.ie%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.joybomb.tw%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.jonaslsa.com%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.filemail.com%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.ddunlimited.net%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.bitsearch.to%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.auctor.tv%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.artixlinux.org%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.army%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.altrosky.nl%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.4.babico.name.tr%3A3131%2Fannounce&tr=udp%3A%2F%2Ftracker-udp.gbitt.info%3A80%2Fannounce&tr=udp%3A%2F%2Ftorrents.artixlinux.org%3A6969%2Fannounce&tr=udp%3A%2F%2Ftorrentclub.space%3A6969%2Fannounce&tr=udp%3A%2F%2Fthouvenin.cloud%3A6969%2Fannounce&tr=udp%3A%2F%2Ftamas3.ynh.fr%3A6969%2Fannounce&tr=udp%3A%2F%2Fsmtp-relay.odysseylabel.com.au%3A6969%2Fannounce&tr=udp%3A%2F%2Fsanincode.com%3A6969%2Fannounce&tr=udp%3A%2F%2Frun.publictracker.xyz%3A6969%2Fannounce&tr=udp%3A%2F%2Frun-2.publictracker.xyz%3A6969%2Fannounce&tr=udp%3A%2F%2Frep-art.ynh.fr%3A6969%2Fannounce&tr=udp%3A%2F%2Frekcart.duckdns.org%3A15480%2Fannounce&tr=udp%3A%2F%2Fqtstm32fan.ru%3A6969%2Fannounce&tr=udp%3A%2F%2Fpublic.publictracker.xyz%3A6969%2Fannounce&tr=udp%3A%2F%2Fpsyco.fr%3A6969%2Fannounce&tr=udp%3A%2F%2Fopen.tracker.ink%3A6969%2Fannounce&tr=udp%3A%2F%2Fopen.free-tracker.ga%3A6969%2Fannounce&tr=udp%3A%2F%2Fopen.dstud.io%3A6969%2Fannounce&tr=udp%3A%2F%2Fnew-line.net%3A6969%2Fannounce&tr=udp%3A%2F%2Fmoonburrow.club%3A6969%2Fannounce&tr=udp%3A%2F%2Fmirror.aptus.co.tz%3A6969%2Fannounce&tr=udp%3A%2F%2Fmail.zasaonsk.ga%3A6969%2Fannounce&tr=udp%3A%2F%2Fmail.artixlinux.org%3A6969%2Fannounce&tr=udp%3A%2F%2Fmadiator.com%3A6969%2Fannounce&tr=udp%3A%2F%2Fleefafa.tk%3A6969%2Fannounce&tr=udp%3A%2F%2Flaze.cc%3A6969%2Fannounce&tr=udp%3A%2F%2Fkokodayo.site%3A6969%2Fannounce&tr=udp%3A%2F%2Fkeke.re%3A6969%2Fannounce&tr=udp%3A%2F%2Fhtz3.noho.st%3A6969%2Fannounce&tr=udp%3A%2F%2Ffh2.cmp-gaming.com%3A6969%2Fannounce&tr=udp%3A%2F%2Ff1sh.de%3A6969%2Fannounce&tr=udp%3A%2F%2Fepider.me%3A6969%2Fannounce&tr=udp%3A%2F%2Felementsbrowser.com%3A6969%2Fannounce&tr=udp%3A%2F%2Fdownload.nerocloud.me%3A6969%2Fannounce&tr=udp%3A%2F%2Fcutscloud.duckdns.org%3A6969%2Fannounce&tr=udp%3A%2F%2Fconcen.org%3A6969%2Fannounce&tr=udp%3A%2F%2Fchouchou.top%3A8080%2Fannounce&tr=udp%3A%2F%2Fcarr.codes%3A6969%2Fannounce&tr=udp%3A%2F%2Fcamera.lei001.com%3A6969%2Fannounce&tr=udp%3A%2F%2Fbuddyfly.top%3A6969%2Fannounce&tr=udp%3A%2F%2Fbubu.mapfactor.com%3A6969%2Fannounce&tr=udp%3A%2F%2Fbt.ktrackers.com%3A6666%2Fannounce&tr=udp%3A%2F%2Fblack-bird.ynh.fr%3A6969%2Fannounce&tr=udp%3A%2F%2Fben.kerbertools.xyz%3A6969%2Fannounce&tr=udp%3A%2F%2Fbananas.space%3A6969%2Fannounce&tr=udp%3A%2F%2Fastrr.ru%3A6969%2Fannounce&tr=udp%3A%2F%2Fapp.icon256.com%3A8000%2Fannounce&tr=udp%3A%2F%2Fadmin.videoenpoche.info%3A6969%2Fannounce&tr=udp%3A%2F%2Fadmin.52ywp.com%3A6969%2Fannounce&tr=udp%3A%2F%2Faarsen.me%3A6969%2Fannounce&tr=udp%3A%2F%2F960303.xyz%3A6969%2Fannounce&tr=https%3A%2F%2Fxtremex.herokuapp.com%3A443%2Fannounce&tr=https%3A%2F%2Ftracker2.ctix.cn%3A443%2Fannounce&tr=https%3A%2F%2Ftracker1.520.jp%3A443%2Fannounce&tr=https%3A%2F%2Ftracker.tamersunion.org%3A443%2Fannounce&tr=https%3A%2F%2Ftracker.kuroy.me%3A443%2Fannounce&tr=https%3A%2F%2Ftracker.gbitt.info%3A443%2Fannounce&tr=https%3A%2F%2Ftracker.foreverpirates.co%3A443%2Fannounce&tr=https%3A%2F%2Ftracker.expli.top%3A443%2Fannounce&tr=https%3A%2F%2Ftr.abir.ga%3A443%2Fannounce&tr=https%3A%2F%2Ftr.abiir.top%3A443%2Fannounce&tr=https%3A%2F%2F1337.abcvg.info%3A443%2Fannounce&tr=http%3A%2F%2Fwepzone.net%3A6969%2Fannounce&tr=http%3A%2F%2Ftracker4.itzmx.com%3A2710%2Fannounce&tr=http%3A%2F%2Ftracker3.itzmx.com%3A6961%2Fannounce&tr=http%3A%2F%2Ftracker3.ctix.cn%3A8080%2Fannounce&tr=http%3A%2F%2Ftracker1.itzmx.com%3A8080%2Fannounce&tr=http%3A%2F%2Ftracker1.bt.moack.co.kr%3A80%2Fannounce&tr=http%3A%2F%2Ftracker.skyts.net%3A6969%2Fannounce&tr=http%3A%2F%2Ftracker.lelux.fi%3A80%2Fannounce&tr=http%3A%2F%2Ftracker.gbitt.info%3A80%2Fannounce&tr=http%3A%2F%2Ftracker.edkj.club%3A6969%2Fannounce&tr=http%3A%2F%2Ftracker.bt4g.com%3A2095%2Fannounce&tr=http%3A%2F%2Ftorrenttracker.nwc.acsalaska.net%3A6969%2Fannounce&tr=http%3A%2F%2Ft.acg.rip%3A6699%2Fannounce&tr=http%3A%2F%2Fopen.tracker.ink%3A6969%2Fannounce&tr=http%3A%2F%2Fopen.acgnxtracker.com%3A80%2Fannounce&tr=http%3A%2F%2Fjp.moeweb.pw%3A6969%2Fannounce&tr=http%3A%2F%2Fincine.ru%3A6969%2Fannounce&tr=http%3A%2F%2Ffxtt.ru%3A80%2Fannounce&tr=http%3A%2F%2Fbt.okmp3.ru%3A2710%2Fannounce&tr=http%3A%2F%2F1337.abcvg.info%3A80%2Fannounce" + + # libtorrent params + ses = lt.session() + params = lt.parse_magnet_uri(uri) + params.save_path = save_path + handle: lt.torrent_handle = ses.add_torrent(params) + + # download roms as long as state is not seeding + timeit = 0 + while handle.status().state not in {4, 5}: + if timeit >= 180: + raise RuntimeError( + "Terminating attempt to download ROMs after 180 seconds, this has failed, please report it." + ) + elif timeit % 5 == 0: + status: lt.torrent_status = handle.status() + print( + f"time={timeit}/180 seconds - Trying to download atari roms\n" + f"\ttotal downloaded bytes={status.total_download}\n" + f"\ttotal payload download={status.total_payload_download}\n" + f"\ttotal failed bytes={status.total_failed_bytes}", + file=sys.stderr, + ) + + # some sleep helps + time.sleep(1.0) + timeit += 1 + + return save_path + + +if __name__ == "__main__": + print(torrent_tar()) diff --git a/setup.cfg b/setup.cfg index 7608867..38ed58e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] license = MIT -license_file = LICENSE.txt +license_files = LICENSE.txt author = Farama Foundation author_email = contact@farama.org version = file: version.txt diff --git a/src/AutoROM.py b/src/AutoROM.py index 6001321..d800331 100644 --- a/src/AutoROM.py +++ b/src/AutoROM.py @@ -1,25 +1,28 @@ #!/usr/bin/env python3 -import time +import hashlib +import io import os +import pathlib import sys -import requests -import warnings -import hashlib import tarfile -import pathlib +import time +import warnings + import click -import io +import requests if os.name == "nt": try: import libtorrent as lt except ImportError as e: - raise ImportError("It seems that you are trying to install the Atari ROMs on Windows. While this is not supported, the DLL error can be solved by installing the OpenSSL DLLs from: https://slproweb.com/products/Win32OpenSSL.html") from e + raise ImportError( + "It seems that you are trying to install the Atari ROMs on Windows. While this is not supported, the DLL error can be solved by installing the OpenSSL DLLs from: https://slproweb.com/products/Win32OpenSSL.html" + ) from e else: import libtorrent as lt -from typing import Dict from collections import namedtuple +from typing import Dict if sys.version_info < (3, 9): import importlib_resources as resources @@ -145,12 +148,11 @@ 4: "finished", 5: "seeding", 6: "error, please report", - 7: "checking resumedata" + 7: "checking resumedata", } -def torrent_tar_to_buffer(): - +def torrent_tar(): # specify the save path save_path = os.path.dirname(__file__) save_file = os.path.join(save_path, "./Roms.tar.gz") @@ -167,16 +169,27 @@ def torrent_tar_to_buffer(): # download roms as long as state is not seeding timeit = 0 while handle.status().state not in {4, 5}: - if timeit >= 180: - raise RuntimeError("Terminating attempt to download ROMs after 180 seconds, this has failed, please report it.") - elif timeit % 5 == 0: + if timeit >= 360: + raise RuntimeError( + "Terminating attempt to download ROMs after 180 seconds, this has failed, please report it." + ) + + if timeit % 5 == 0: + if timeit >= 180: + print( + "Have been attempting to download for more than 180 seconds, consider terminating?", + file=sys.stderr, + ) + status: lt.torrent_status = handle.status() - print(f"time={timeit}/180 seconds - Trying to download atari roms\n" - f"\tcurrent status={status_meaning.get(status.state, 'unknown')} ({status.state})\n" - f"\ttotal downloaded bytes={status.total_download}\n" - f"\ttotal payload download={status.total_payload_download}\n" - f"\ttotal failed bytes={status.total_failed_bytes}", - file=sys.stderr) + print( + f"time={timeit} / 180 seconds - Trying to download atari roms\n" + f"\tcurrent status={status_meaning.get(status.state, 'unknown')} ({status.state})\n" + f"\ttotal downloaded bytes={status.total_download}\n" + f"\ttotal payload download={status.total_payload_download}\n" + f"\ttotal failed bytes={status.total_failed_bytes}", + file=sys.stderr, + ) # some sleep helps time.sleep(1.0) @@ -184,18 +197,15 @@ def torrent_tar_to_buffer(): # seed for 20 seconds to help the network if handle.status().state in {4, 5}: - print("Download complete, seeding for 20 seconds to assist torrent network.", file=sys.stderr) + print( + "Download complete, seeding for 20 seconds to assist torrent network.", + file=sys.stderr, + ) time.sleep(20.0) print("Seeding completed.", file=sys.stderr) - # read it as a buffer - with open(save_file, "rb") as fh: - buffer = io.BytesIO(fh.read()) - - # delete the download - os.remove(save_file) + return save_file - return buffer def verify_installation(package, checksum_keys): for file in os.listdir(package): @@ -204,7 +214,7 @@ def verify_installation(package, checksum_keys): continue rom_path = os.path.join(package, file) - hash = hashlib.md5(open(rom_path,'rb').read()).hexdigest() + hash = hashlib.md5(open(rom_path, "rb").read()).hexdigest() if not hash in checksum_keys: return False @@ -213,6 +223,7 @@ def verify_installation(package, checksum_keys): return len(checksum_keys) == 0 + # Extract each valid ROM into each dir in installation_dirs def extract_roms_from_tar(buffer, packages, checksum_map, quiet): with tarfile.open(fileobj=buffer) as tarfp: @@ -298,7 +309,7 @@ def find_supported_packages(): return installation_dirs -def main(accept_license, install_dir, quiet): +def main(accept_license, source_file, install_dir, quiet): if install_dir is not None: packages = [ SupportedPackage(pathlib.Path(install_dir), "{rom}.bin", lambda _: True) @@ -307,8 +318,7 @@ def main(accept_license, install_dir, quiet): packages = find_supported_packages() if len(packages) == 0: - print("Unable to find ale-py or multi-ale-py, quitting.") - quit() + raise LookupError("Unable to find ale-py or multi-ale-py, quitting.") print("AutoROM will download the Atari 2600 ROMs.\nThey will be installed to:") for package in packages: @@ -321,7 +331,7 @@ def main(accept_license, install_dir, quiet): "I agree to not distribute these ROMs and wish to proceed:" ) if not click.confirm(license_msg, default=True): - quit() + return # Make sure directories exist for package in packages: @@ -331,16 +341,25 @@ def main(accept_license, install_dir, quiet): # Create copy of checksum map which will be mutated checksum_map = dict(CHECKSUM_MAP) try: - if all([verify_installation(package.path, checksum_map.keys())] for package in packages): - quit() - buffer = torrent_tar_to_buffer() - extract_roms_from_tar(buffer, packages, checksum_map, quiet) + if all( + verify_installation(package.path, list(checksum_map.keys())) + for package in packages + ): + return + + with open(torrent_tar() if source_file is None else source_file, "rb") as fh: + buffer = io.BytesIO(fh.read()) + extract_roms_from_tar(buffer, packages, checksum_map, quiet) + except tarfile.ReadError: - print("Failed to read tar archive. Check your network connection?") - quit() + if source_file is None: + print("Failed to read tar archive. Check your network connection?") + else: + print("Failed to read tar archive. Verify your source file?") + return except requests.ConnectionError: print("Network connection error. Check your network settings?") - quit() + return # Print missing ROMs for rom in checksum_map.values(): @@ -356,22 +375,29 @@ def main(accept_license, install_dir, quiet): is_flag=True, default=False, type=bool, - help="Accept license agreement", + help="Accept license agreement.", ) @click.option( "-d", "--install-dir", default=None, type=click.Path(exists=True), - help="User specified install directory", + help="User specified install directory.", +) +@click.option( + "-s", + "--source-file", + default=None, + type=click.Path(exists=True), + help="User specified .tar.gz source file.", ) @click.option( "--quiet", is_flag=True, default=False, help="Suppress installation output." ) -def cli(accept_license, install_dir, quiet): - main(accept_license, install_dir, quiet) +def cli(accept_license, source_file, install_dir, quiet): + main(accept_license, source_file, install_dir, quiet) if __name__ == "__main__": cli() - # torrent_tar_to_buffer() + # main(True, None, None, False) diff --git a/version.txt b/version.txt index 7d85683..d1d899f 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.5.4 +0.5.5