diff --git a/.github/workflows/windows_exe.yml b/.github/workflows/windows_exe.yml new file mode 100644 index 0000000..6780950 --- /dev/null +++ b/.github/workflows/windows_exe.yml @@ -0,0 +1,47 @@ +name: Windows EXE build + +on: [push, pull_request] + +jobs: + build: + runs-on: windows-latest + steps: + - name: Checkout Instaloader repository + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v1 + with: + python-version: 3.8 + architecture: x64 + - name: Get the tagged version + id: get_version + env: + GITHUB_REF: ${{ github.ref }} + run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\/v/} + shell: bash + - name: Run custom python script for EXE creation + env: + VERSION_TAG: v${{ steps.get_version.outputs.VERSION }} + run: python deploy/windows/create_exe.py + - name: Create draft release + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v${{ steps.get_version.outputs.VERSION }} + release_name: Version ${{ steps.get_version.outputs.VERSION }} + draft: true + prerelease: contains(steps.get_version.outputs.VERSION, 'a') || contains(steps.get_version.outputs.VERSION, 'b') || contains(steps.get_version.outputs.VERSION, 'c') + - name: Upload EXE as release asset + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + id: upload-release-asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./instaloader-v${{ steps.get_version.outputs.VERSION }}-windows-standalone.zip + asset_name: instaloader-v${{ steps.get_version.outputs.VERSION }}-windows-standalone.zip + asset_content_type: application/zip diff --git a/Pipfile b/Pipfile index 4a82320..1cfe444 100644 --- a/Pipfile +++ b/Pipfile @@ -7,6 +7,10 @@ verify_ssl = true pylint = "*" mypy = "*" sphinx = "*" +pyinstaller = "*" +pefile = "*" +pywin32-ctypes = "*" +psutil = "*" [packages] requests = "*" diff --git a/Pipfile.lock b/Pipfile.lock index ae9cffc..43c9e72 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "dfbaa1642735b07bdcc118cb0c9328817b1d558af626b696593db099df863b39" + "sha256": "33408a60fda1a56bf680462fad21643ae0159bc13f3b85d8f10ebf3eef7d8725" }, "pipfile-spec": 6, "requires": {}, @@ -59,6 +59,13 @@ ], "version": "==0.7.12" }, + "altgraph": { + "hashes": [ + "sha256:1f05a47122542f97028caf78775a095fbe6a2699b5089de8477eb583167d69aa", + "sha256:c623e5f3408ca61d4016f23a681b9adb100802ca3e3da5e718915a9e4052cebe" + ], + "version": "==0.17" + }, "astroid": { "hashes": [ "sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a", @@ -94,6 +101,12 @@ ], "version": "==0.16" }, + "future": { + "hashes": [ + "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" + ], + "version": "==0.18.2" + }, "idna": { "hashes": [ "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", @@ -117,10 +130,10 @@ }, "jinja2": { "hashes": [ - "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", - "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de" + "sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250", + "sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49" ], - "version": "==2.10.3" + "version": "==2.11.1" }, "lazy-object-proxy": { "hashes": [ @@ -227,6 +240,30 @@ ], "version": "==20.1" }, + "pefile": { + "hashes": [ + "sha256:a5d6e8305c6b210849b47a6174ddf9c452b2888340b8177874b862ba6c207645" + ], + "index": "pypi", + "version": "==2019.4.18" + }, + "psutil": { + "hashes": [ + "sha256:094f899ac3ef72422b7e00411b4ed174e3c5a2e04c267db6643937ddba67a05b", + "sha256:10b7f75cc8bd676cfc6fa40cd7d5c25b3f45a0e06d43becd7c2d2871cbb5e806", + "sha256:1b1575240ca9a90b437e5a40db662acd87bbf181f6aa02f0204978737b913c6b", + "sha256:21231ef1c1a89728e29b98a885b8e0a8e00d09018f6da5cdc1f43f988471a995", + "sha256:28f771129bfee9fc6b63d83a15d857663bbdcae3828e1cb926e91320a9b5b5cd", + "sha256:70387772f84fa5c3bb6a106915a2445e20ac8f9821c5914d7cbde148f4d7ff73", + "sha256:b560f5cd86cf8df7bcd258a851ca1ad98f0d5b8b98748e877a0aec4e9032b465", + "sha256:b74b43fecce384a57094a83d2778cdfc2e2d9a6afaadd1ebecb2e75e0d34e10d", + "sha256:e85f727ffb21539849e6012f47b12f6dd4c44965e56591d8dec6e8bc9ab96f4a", + "sha256:fd2e09bb593ad9bdd7429e779699d2d47c1268cbde4dda95fcd1bd17544a0217", + "sha256:ffad8eb2ac614518bbe3c0b8eb9dffdb3a8d2e3a7d5da51c5b974fb723a5c5aa" + ], + "index": "pypi", + "version": "==5.6.7" + }, "pygments": { "hashes": [ "sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b", @@ -234,6 +271,13 @@ ], "version": "==2.5.2" }, + "pyinstaller": { + "hashes": [ + "sha256:3730fa80d088f8bb7084d32480eb87cbb4ddb64123363763cf8f2a1378c1c4b7" + ], + "index": "pypi", + "version": "==3.6" + }, "pylint": { "hashes": [ "sha256:3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd", @@ -256,6 +300,14 @@ ], "version": "==2019.3" }, + "pywin32-ctypes": { + "hashes": [ + "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942", + "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98" + ], + "index": "pypi", + "version": "==0.2.0" + }, "requests": { "hashes": [ "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", diff --git a/deploy/windows/create_exe.py b/deploy/windows/create_exe.py new file mode 100644 index 0000000..7b5ca6f --- /dev/null +++ b/deploy/windows/create_exe.py @@ -0,0 +1,86 @@ +import hashlib +import os +import re +import shutil +import subprocess +import sys + +# copy the required files into repo root +shutil.copy('docs/favicon.ico', '.') +shutil.copy('deploy/windows/instaloader.spec', '.') +shutil.unpack_archive('deploy/windows/ps', '.', 'xztar') + +code = """ +import psutil +import subprocess + +def __main(): + grandparentpid = psutil.Process(os.getppid()).ppid() + grandparentpidsearchstring = ' ' + str(grandparentpid) + ' ' + if hasattr(sys, "_MEIPASS"): + ps = os.path.join(sys._MEIPASS, 'tasklist.exe') + else: + ps = 'tasklist' + popen = subprocess.Popen(ps, stdout=subprocess.PIPE, universal_newlines=True) + for examine in iter(popen.stdout.readline, ""): + if grandparentpidsearchstring in examine: + pname = examine + break + popen.stdout.close() + return_code = popen.wait() + if return_code: + raise subprocess.CalledProcessError(return_code, ps) + if pname[0:12] == 'explorer.exe': + subprocess.Popen("cmd /K \\\"{0}\\\"".format(os.path.splitext(os.path.basename(sys.argv[0]))[0])) + else: + main() + + +if __name__ == "__main__": + __main() +""" + +with open('instaloader/__main__.py', 'r') as f: + # adjust imports for changed file structure + regex = re.compile(r'from (?:(\.[^ ]+ )|\.( ))import') + lines = [regex.sub(r'from instaloader\1\2import', line) for line in f.readlines()] + + # insert code for magic exe behavior + index = lines.index('if __name__ == "__main__":\n') + code_lines = [cl + '\n' for cl in code.splitlines()] + for i, code_line in enumerate(code_lines): + if i + index < len(lines): + lines[i + index] = code_line + else: + lines.extend(code_lines[i:]) + break + +with open('__main__.py', 'w+') as f: + f.writelines(lines) + +# install dependencies and invoke PyInstaller +commands = ["pip install pipenv==2018.11.26", + "pipenv sync --dev", + "pipenv run pyinstaller --log-level=DEBUG instaloader.spec"] + +for command in commands: + print() + print('#' * (len(command) + 6)) + print('## {} ##'.format(command)) + print('#' * (len(command) + 6)) + print(flush=True) + err = subprocess.Popen(command).wait() + if err != 0: + sys.exit(err) + +# calculate and store MD5 hash for created executable +hash_md5 = hashlib.md5() +with open('dist/instaloader.exe', 'rb') as f: + for chunk in iter(lambda: f.read(4096), b''): + hash_md5.update(chunk) + +with open('dist/instaloader.exe.md5', 'w+') as f: + f.write('{} *instaloader.exe\n'.format(hash_md5.hexdigest())) + +# Create ZIP file +shutil.make_archive('instaloader-{}-windows-standalone'.format(os.getenv('VERSION_TAG')), 'zip', 'dist') diff --git a/deploy/windows/instaloader.spec b/deploy/windows/instaloader.spec new file mode 100644 index 0000000..42a8f67 --- /dev/null +++ b/deploy/windows/instaloader.spec @@ -0,0 +1,28 @@ +# -*- mode: python -*- + +block_cipher = None + + +a = Analysis(['__main__.py'], + binaries=[], + datas=[('tasklist.exe', '.'), ('framedyn.dll', '.')], + hiddenimports=[], + hookspath=[], + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher) +pyz = PYZ(a.pure, a.zipped_data, + cipher=block_cipher) +exe = EXE(pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + name='instaloader', + debug=False, + strip=False, + upx=False, + runtime_tmpdir=None, + console=True , icon='favicon.ico') diff --git a/deploy/windows/ps b/deploy/windows/ps new file mode 100644 index 0000000..6760d35 Binary files /dev/null and b/deploy/windows/ps differ