]> jfr.im git - yt-dlp.git/blame - yt_dlp/update.py
[build] Improve release process (#880)
[yt-dlp.git] / yt_dlp / update.py
CommitLineData
15938ab6
PH
1from __future__ import unicode_literals
2
c19bc311 3import hashlib
d5ed35b6 4import json
ce02ed60 5import os
e5813e53 6import platform
d2790370 7import subprocess
46353f67 8import sys
c19bc311 9import traceback
d5ed35b6
FV
10from zipimport import zipimporter
11
bfe2b8cf 12from .compat import compat_realpath
c0384f22 13from .utils import encode_compat_str
d3d3e2e3 14
d5ed35b6
FV
15from .version import __version__
16
5f6a1245 17
3dd264bf 18''' # Not signed
d5ed35b6 19def rsa_verify(message, signature, key):
d5ed35b6 20 from hashlib import sha256
15938ab6 21 assert isinstance(message, bytes)
4d318be1
FV
22 byte_size = (len(bin(key[0])) - 2 + 8 - 1) // 8
23 signature = ('%x' % pow(int(signature, 16), key[1], key[0])).encode()
24 signature = (byte_size * 2 - len(signature)) * b'0' + signature
25 asn1 = b'3031300d060960864801650304020105000420'
26 asn1 += sha256(message).hexdigest().encode()
27 if byte_size < len(asn1) // 2 + 11:
5f6a1245 28 return False
4d318be1
FV
29 expected = b'0001' + (byte_size - len(asn1) // 2 - 3) * b'ff' + b'00' + asn1
30 return expected == signature
3dd264bf 31'''
d5ed35b6 32
d7386f62 33
4c88ff87 34def detect_variant():
35 if hasattr(sys, 'frozen') and getattr(sys, '_MEIPASS', None):
36 if sys._MEIPASS == os.path.dirname(sys.executable):
37 return 'dir'
38 return 'exe'
39 elif isinstance(globals().get('__loader__'), zipimporter):
40 return 'zip'
41 elif os.path.basename(sys.argv[0]) == '__main__.py':
42 return 'source'
43 return 'unknown'
44
45
0c3e5f49 46def update_self(to_screen, verbose, opener):
c19bc311 47 ''' Exists for backward compatibility. Use run_update(ydl) instead '''
48
49 printfn = to_screen
50
51 class FakeYDL():
52 _opener = opener
53 to_screen = printfn
54
55 @staticmethod
56 def report_warning(msg, *args, **kwargs):
57 return printfn('WARNING: %s' % msg, *args, **kwargs)
58
59 @staticmethod
60 def report_error(msg, tb=None):
61 printfn('ERROR: %s' % msg)
62 if not verbose:
63 return
64 if tb is None:
65 # Copied from YoutubeDl.trouble
66 if sys.exc_info()[0]:
67 tb = ''
68 if hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
69 tb += ''.join(traceback.format_exception(*sys.exc_info()[1].exc_info))
70 tb += encode_compat_str(traceback.format_exc())
71 else:
72 tb_data = traceback.format_list(traceback.extract_stack())
73 tb = ''.join(tb_data)
74 if tb:
75 printfn(tb)
76
77 return run_update(FakeYDL())
78
79
80def run_update(ydl):
e5813e53 81 """
82 Update the program file with the latest version from the repository
83 Returns whether the program should terminate
84 """
d5ed35b6 85
7a5c1cfe 86 JSON_URL = 'https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest'
d5ed35b6 87
c19bc311 88 def report_error(msg, network=False, expected=False, delim=';'):
89 if network:
90 msg += '%s Visit https://github.com/yt-dlp/yt-dlp/releases/latest' % delim
91 ydl.report_error(msg, tb='' if network or expected else None)
92
44f705d0 93 def calc_sha256sum(path):
fa57af1e
U
94 h = hashlib.sha256()
95 b = bytearray(128 * 1024)
96 mv = memoryview(b)
44f705d0 97 with open(os.path.realpath(path), 'rb', buffering=0) as f:
fa57af1e
U
98 for n in iter(lambda: f.readinto(mv), 0):
99 h.update(mv[:n])
100 return h.hexdigest()
101
4c88ff87 102 ERRORS = {
103 'exe': None,
104 'zip': None,
105 'dir': 'Auto-update is not supported for unpackaged windows executable. Re-download the latest release',
106 'source': 'You cannot update when running from source code',
107 'unknown': 'It looks like you installed yt-dlp with a package manager, pip, setup.py or a tarball. Use that to update',
108 }
109 err = ERRORS.get(detect_variant(), ERRORS['unknown'])
4040428e 110 if err:
111 return report_error(err, expected=True)
d5ed35b6 112
7815e555 113 # sys.executable is set to the full pathname of the exe-file for py2exe
114 # though symlinks are not followed so that we need to do this manually
115 # with help of realpath
116 filename = compat_realpath(sys.executable if hasattr(sys, 'frozen') else sys.argv[0])
c19bc311 117 ydl.to_screen('Current Build Hash %s' % calc_sha256sum(filename))
7815e555 118
d5ed35b6
FV
119 # Download and check versions info
120 try:
c19bc311 121 version_info = ydl._opener.open(JSON_URL).read().decode('utf-8')
3dd264bf 122 version_info = json.loads(version_info)
70a1165b 123 except Exception:
c19bc311 124 return report_error('can\'t obtain versions info. Please try again later ', True, delim='or')
d5ed35b6 125
d7386f62
PH
126 def version_tuple(version_str):
127 return tuple(map(int, version_str.split('.')))
3dd264bf 128
e5813e53 129 version_id = version_info['tag_name']
2e767313 130 if version_tuple(__version__) >= version_tuple(version_id):
c19bc311 131 ydl.to_screen('yt-dlp is up to date (%s)' % __version__)
d7386f62
PH
132 return
133
c19bc311 134 ydl.to_screen('Updating to version ' + version_id + ' ...')
3bf79c75 135
44f705d0 136 version_labels = {
137 'zip_3': '',
44f705d0 138 'exe_64': '.exe',
139 'exe_32': '_x86.exe',
140 }
141
e5813e53 142 def get_bin_info(bin_or_exe, version):
44f705d0 143 label = version_labels['%s_%s' % (bin_or_exe, version)]
c19bc311 144 return next((i for i in version_info['assets'] if i['name'] == 'yt-dlp%s' % label), {})
44f705d0 145
146 def get_sha256sum(bin_or_exe, version):
beb982be 147 filename = 'yt-dlp%s' % version_labels['%s_%s' % (bin_or_exe, version)]
44f705d0 148 urlh = next(
c19bc311 149 (i for i in version_info['assets'] if i['name'] in ('SHA2-256SUMS')),
150 {}).get('browser_download_url')
44f705d0 151 if not urlh:
152 return None
c19bc311 153 hash_data = ydl._opener.open(urlh).read().decode('utf-8')
4c88ff87 154 return dict(ln.split()[::-1] for ln in hash_data.splitlines()).get(filename)
d5ed35b6
FV
155
156 if not os.access(filename, os.W_OK):
c19bc311 157 return report_error('no write permissions on %s' % filename, expected=True)
d5ed35b6 158
3dd264bf 159 # PyInstaller
611c1dd9 160 if hasattr(sys, 'frozen'):
e9297256 161 exe = filename
d5ed35b6
FV
162 directory = os.path.dirname(exe)
163 if not os.access(directory, os.W_OK):
c19bc311 164 return report_error('no write permissions on %s' % directory, expected=True)
b25522ba 165 try:
166 if os.path.exists(filename + '.old'):
167 os.remove(filename + '.old')
168 except (IOError, OSError):
169 return report_error('unable to remove the old version')
d5ed35b6
FV
170
171 try:
e5813e53 172 arch = platform.architecture()[0][:2]
44f705d0 173 url = get_bin_info('exe', arch).get('browser_download_url')
174 if not url:
c19bc311 175 return report_error('unable to fetch updates', True)
176 urlh = ydl._opener.open(url)
d5ed35b6
FV
177 newcontent = urlh.read()
178 urlh.close()
e5813e53 179 except (IOError, OSError, StopIteration):
c19bc311 180 return report_error('unable to download latest version', True)
d5ed35b6
FV
181
182 try:
183 with open(exe + '.new', 'wb') as outf:
184 outf.write(newcontent)
0b63aed8 185 except (IOError, OSError):
c19bc311 186 return report_error('unable to write the new version')
d5ed35b6 187
44f705d0 188 expected_sum = get_sha256sum('exe', arch)
189 if not expected_sum:
c19bc311 190 ydl.report_warning('no hash information found for the release')
44f705d0 191 elif calc_sha256sum(exe + '.new') != expected_sum:
c19bc311 192 report_error('unable to verify the new executable', True)
44f705d0 193 try:
194 os.remove(exe + '.new')
195 except OSError:
c19bc311 196 return report_error('unable to remove corrupt download')
44f705d0 197
d5ed35b6 198 try:
b25522ba 199 os.rename(exe, exe + '.old')
200 except (IOError, OSError):
201 return report_error('unable to move current version')
202 try:
203 os.rename(exe + '.new', exe)
0b63aed8 204 except (IOError, OSError):
c19bc311 205 report_error('unable to overwrite current version')
b25522ba 206 os.rename(exe + '.old', exe)
207 return
208 try:
209 # Continues to run in the background
210 subprocess.Popen(
211 'ping 127.0.0.1 -n 5 -w 1000 & del /F "%s.old"' % exe,
212 shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
213 ydl.to_screen('Updated yt-dlp to version %s' % version_id)
214 return True # Exit app
215 except OSError:
216 report_error('unable to delete old version')
d5ed35b6
FV
217
218 # Zip unix package
219 elif isinstance(globals().get('__loader__'), zipimporter):
220 try:
4040428e 221 url = get_bin_info('zip', '3').get('browser_download_url')
44f705d0 222 if not url:
c19bc311 223 return report_error('unable to fetch updates', True)
224 urlh = ydl._opener.open(url)
d5ed35b6
FV
225 newcontent = urlh.read()
226 urlh.close()
e5813e53 227 except (IOError, OSError, StopIteration):
c19bc311 228 return report_error('unable to download latest version', True)
d5ed35b6 229
4040428e 230 expected_sum = get_sha256sum('zip', '3')
beb982be
NA
231 if not expected_sum:
232 ydl.report_warning('no hash information found for the release')
233 elif hashlib.sha256(newcontent).hexdigest() != expected_sum:
c19bc311 234 return report_error('unable to verify the new zip', True)
44f705d0 235
236 try:
c4a508ab 237 with open(filename, 'wb') as outf:
238 outf.write(newcontent)
239 except (IOError, OSError):
c19bc311 240 return report_error('unable to overwrite current version')
d5ed35b6 241
c19bc311 242 ydl.to_screen('Updated yt-dlp to version %s; Restart yt-dlp to use the new version' % version_id)
3bf79c75 243
5f6a1245 244
3dd264bf 245''' # UNUSED
46a127ee 246def get_notes(versions, fromVersion):
3bf79c75 247 notes = []
5f6a1245 248 for v, vdata in sorted(versions.items()):
3bf79c75
PH
249 if v > fromVersion:
250 notes.extend(vdata.get('notes', []))
46a127ee
PH
251 return notes
252
5f6a1245 253
46a127ee
PH
254def print_notes(to_screen, versions, fromVersion=__version__):
255 notes = get_notes(versions, fromVersion)
3bf79c75 256 if notes:
15938ab6 257 to_screen('PLEASE NOTE:')
3bf79c75
PH
258 for note in notes:
259 to_screen(note)
3dd264bf 260'''