pyinst.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. #!/usr/bin/env python3
  2. import os
  3. import platform
  4. import sys
  5. from PyInstaller.__main__ import run as run_pyinstaller
  6. OS_NAME, MACHINE, ARCH = sys.platform, platform.machine(), platform.architecture()[0][:2]
  7. if MACHINE in ('x86_64', 'AMD64') or ('i' in MACHINE and '86' in MACHINE):
  8. # NB: Windows x86 has MACHINE = AMD64 irrespective of bitness
  9. MACHINE = 'x86' if ARCH == '32' else ''
  10. def main():
  11. opts = parse_options()
  12. version = read_version('yt_dlp/version.py')
  13. onedir = '--onedir' in opts or '-D' in opts
  14. if not onedir and '-F' not in opts and '--onefile' not in opts:
  15. opts.append('--onefile')
  16. name, final_file = exe(onedir)
  17. print(f'Building yt-dlp v{version} for {OS_NAME} {platform.machine()} with options {opts}')
  18. print('Remember to update the version using "devscripts/update-version.py"')
  19. if not os.path.isfile('yt_dlp/extractor/lazy_extractors.py'):
  20. print('WARNING: Building without lazy_extractors. Run '
  21. '"devscripts/make_lazy_extractors.py" to build lazy extractors', file=sys.stderr)
  22. print(f'Destination: {final_file}\n')
  23. opts = [
  24. f'--name={name}',
  25. '--icon=devscripts/logo.ico',
  26. '--upx-exclude=vcruntime140.dll',
  27. '--noconfirm',
  28. *dependency_options(),
  29. *opts,
  30. 'yt_dlp/__main__.py',
  31. ]
  32. print(f'Running PyInstaller with {opts}')
  33. run_pyinstaller(opts)
  34. set_version_info(final_file, version)
  35. def parse_options():
  36. # Compatibility with older arguments
  37. opts = sys.argv[1:]
  38. if opts[0:1] in (['32'], ['64']):
  39. if ARCH != opts[0]:
  40. raise Exception(f'{opts[0]}bit executable cannot be built on a {ARCH}bit system')
  41. opts = opts[1:]
  42. return opts
  43. # Get the version from yt_dlp/version.py without importing the package
  44. def read_version(fname):
  45. with open(fname, encoding='utf-8') as f:
  46. exec(compile(f.read(), fname, 'exec'))
  47. return locals()['__version__']
  48. def exe(onedir):
  49. """@returns (name, path)"""
  50. name = '_'.join(filter(None, (
  51. 'yt-dlp',
  52. {'win32': '', 'darwin': 'macos'}.get(OS_NAME, OS_NAME),
  53. MACHINE
  54. )))
  55. return name, ''.join(filter(None, (
  56. 'dist/',
  57. onedir and f'{name}/',
  58. name,
  59. OS_NAME == 'win32' and '.exe'
  60. )))
  61. def version_to_list(version):
  62. version_list = version.split('.')
  63. return list(map(int, version_list)) + [0] * (4 - len(version_list))
  64. def dependency_options():
  65. # Due to the current implementation, these are auto-detected, but explicitly add them just in case
  66. dependencies = [pycryptodome_module(), 'mutagen', 'brotli', 'certifi', 'websockets']
  67. excluded_modules = ['test', 'ytdlp_plugins', 'youtube_dl', 'youtube_dlc']
  68. yield from (f'--hidden-import={module}' for module in dependencies)
  69. yield '--collect-submodules=websockets'
  70. yield from (f'--exclude-module={module}' for module in excluded_modules)
  71. def pycryptodome_module():
  72. try:
  73. import Cryptodome # noqa: F401
  74. except ImportError:
  75. try:
  76. import Crypto # noqa: F401
  77. print('WARNING: Using Crypto since Cryptodome is not available. '
  78. 'Install with: pip install pycryptodomex', file=sys.stderr)
  79. return 'Crypto'
  80. except ImportError:
  81. pass
  82. return 'Cryptodome'
  83. def set_version_info(exe, version):
  84. if OS_NAME == 'win32':
  85. windows_set_version(exe, version)
  86. def windows_set_version(exe, version):
  87. from PyInstaller.utils.win32.versioninfo import (
  88. FixedFileInfo,
  89. SetVersion,
  90. StringFileInfo,
  91. StringStruct,
  92. StringTable,
  93. VarFileInfo,
  94. VarStruct,
  95. VSVersionInfo,
  96. )
  97. version_list = version_to_list(version)
  98. suffix = MACHINE and f'_{MACHINE}'
  99. SetVersion(exe, VSVersionInfo(
  100. ffi=FixedFileInfo(
  101. filevers=version_list,
  102. prodvers=version_list,
  103. mask=0x3F,
  104. flags=0x0,
  105. OS=0x4,
  106. fileType=0x1,
  107. subtype=0x0,
  108. date=(0, 0),
  109. ),
  110. kids=[
  111. StringFileInfo([StringTable('040904B0', [
  112. StringStruct('Comments', 'yt-dlp%s Command Line Interface' % suffix),
  113. StringStruct('CompanyName', 'https://github.com/yt-dlp'),
  114. StringStruct('FileDescription', 'yt-dlp%s' % (MACHINE and f' ({MACHINE})')),
  115. StringStruct('FileVersion', version),
  116. StringStruct('InternalName', f'yt-dlp{suffix}'),
  117. StringStruct('LegalCopyright', 'pukkandan.ytdlp@gmail.com | UNLICENSE'),
  118. StringStruct('OriginalFilename', f'yt-dlp{suffix}.exe'),
  119. StringStruct('ProductName', f'yt-dlp{suffix}'),
  120. StringStruct(
  121. 'ProductVersion', f'{version}{suffix} on Python {platform.python_version()}'),
  122. ])]), VarFileInfo([VarStruct('Translation', [0, 1200])])
  123. ]
  124. ))
  125. if __name__ == '__main__':
  126. main()