Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Setup: more typing #4089

Merged
merged 1 commit into from
Oct 25, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 54 additions & 44 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import shutil
import sys
import sysconfig
import typing
import warnings
import zipfile
import urllib.request
Expand All @@ -14,14 +13,14 @@
import threading
import subprocess

from collections.abc import Iterable
from hashlib import sha3_512
from pathlib import Path
from typing import Dict, Iterable, List, Optional, Sequence, Set, Tuple, Union


# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
requirement = 'cx-Freeze==7.2.0'
try:
requirement = 'cx-Freeze==7.2.0'
import pkg_resources
try:
pkg_resources.require(requirement)
Expand All @@ -30,7 +29,7 @@
install_cx_freeze = True
except ImportError:
install_cx_freeze = True
pkg_resources = None # type: ignore [assignment]
pkg_resources = None # type: ignore[assignment]

if install_cx_freeze:
# check if pip is available
Expand Down Expand Up @@ -61,7 +60,7 @@


# On Python < 3.10 LogicMixin is not currently supported.
non_apworlds: set = {
non_apworlds: Set[str] = {
"A Link to the Past",
"Adventure",
"ArchipIDLE",
Expand All @@ -84,7 +83,7 @@
if sys.version_info < (3,10):
non_apworlds.add("Hollow Knight")

def download_SNI():
def download_SNI() -> None:
print("Updating SNI")
machine_to_go = {
"x86_64": "amd64",
Expand Down Expand Up @@ -114,8 +113,8 @@ def download_SNI():
if source_url and source_url.endswith(".zip"):
with urllib.request.urlopen(source_url) as download:
with zipfile.ZipFile(io.BytesIO(download.read()), "r") as zf:
for member in zf.infolist():
zf.extract(member, path="SNI")
for zf_member in zf.infolist():
zf.extract(zf_member, path="SNI")
print(f"Downloaded SNI from {source_url}")

elif source_url and (source_url.endswith(".tar.xz") or source_url.endswith(".tar.gz")):
Expand All @@ -129,11 +128,13 @@ def download_SNI():
raise ValueError(f"Unexpected file '{member.name}' in {source_url}")
elif member.isdir() and not sni_dir:
sni_dir = member.name
elif member.isfile() and not sni_dir or not member.name.startswith(sni_dir):
elif member.isfile() and not sni_dir or sni_dir and not member.name.startswith(sni_dir):
raise ValueError(f"Expected folder before '{member.name}' in {source_url}")
elif member.isfile() and sni_dir:
tf.extract(member)
# sadly SNI is in its own folder on non-windows, so we need to rename
if not sni_dir:
raise ValueError("Did not find SNI in archive")
shutil.rmtree("SNI", True)
os.rename(sni_dir, "SNI")
print(f"Downloaded SNI from {source_url}")
Expand All @@ -145,7 +146,7 @@ def download_SNI():
print(f"No SNI found for system spec {platform_name} {machine_name}")


signtool: typing.Optional[str]
signtool: Optional[str]
if os.path.exists("X:/pw.txt"):
print("Using signtool")
with open("X:/pw.txt", encoding="utf-8-sig") as f:
Expand Down Expand Up @@ -197,13 +198,13 @@ def resolve_icon(icon_name: str):
extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else []


def remove_sprites_from_folder(folder):
def remove_sprites_from_folder(folder: Path) -> None:
for file in os.listdir(folder):
if file != ".gitignore":
os.remove(folder / file)


def _threaded_hash(filepath):
def _threaded_hash(filepath: Union[str, Path]) -> str:
hasher = sha3_512()
hasher.update(open(filepath, "rb").read())
return base64.b85encode(hasher.digest()).decode()
Expand All @@ -217,11 +218,11 @@ class BuildCommand(setuptools.command.build.build):
yes: bool
last_yes: bool = False # used by sub commands of build

def initialize_options(self):
def initialize_options(self) -> None:
super().initialize_options()
type(self).last_yes = self.yes = False

def finalize_options(self):
def finalize_options(self) -> None:
super().finalize_options()
type(self).last_yes = self.yes

Expand All @@ -233,27 +234,27 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
('extra-data=', None, 'Additional files to add.'),
]
yes: bool
extra_data: Iterable # [any] not available in 3.8
extra_libs: Iterable # work around broken include_files
extra_data: Iterable[str]
extra_libs: Iterable[str] # work around broken include_files

buildfolder: Path
libfolder: Path
library: Path
buildtime: datetime.datetime

def initialize_options(self):
def initialize_options(self) -> None:
super().initialize_options()
self.yes = BuildCommand.last_yes
self.extra_data = []
self.extra_libs = []

def finalize_options(self):
def finalize_options(self) -> None:
super().finalize_options()
self.buildfolder = self.build_exe
self.libfolder = Path(self.buildfolder, "lib")
self.library = Path(self.libfolder, "library.zip")

def installfile(self, path, subpath=None, keep_content: bool = False):
def installfile(self, path: Path, subpath: Optional[Union[str, Path]] = None, keep_content: bool = False) -> None:
folder = self.buildfolder
if subpath:
folder /= subpath
Expand All @@ -268,7 +269,7 @@ def installfile(self, path, subpath=None, keep_content: bool = False):
else:
print('Warning,', path, 'not found')

def create_manifest(self, create_hashes=False):
def create_manifest(self, create_hashes: bool = False) -> None:
# Since the setup is now split into components and the manifest is not,
# it makes most sense to just remove the hashes for now. Not aware of anyone using them.
hashes = {}
Expand All @@ -290,7 +291,7 @@ def create_manifest(self, create_hashes=False):
json.dump(manifest, open(manifestpath, "wt"), indent=4)
print("Created Manifest")

def run(self):
def run(self) -> None:
# start downloading sni asap
sni_thread = threading.Thread(target=download_SNI, name="SNI Downloader")
sni_thread.start()
Expand Down Expand Up @@ -341,7 +342,7 @@ def run(self):

# post build steps
if is_windows: # kivy_deps is win32 only, linux picks them up automatically
from kivy_deps import sdl2, glew
from kivy_deps import sdl2, glew # type: ignore
for folder in sdl2.dep_bins + glew.dep_bins:
shutil.copytree(folder, self.libfolder, dirs_exist_ok=True)
print(f"copying {folder} -> {self.libfolder}")
Expand All @@ -362,7 +363,7 @@ def run(self):
self.installfile(Path(data))

# kivi data files
import kivy
import kivy # type: ignore[import-untyped]
shutil.copytree(os.path.join(os.path.dirname(kivy.__file__), "data"),
self.buildfolder / "data",
dirs_exist_ok=True)
Expand All @@ -372,7 +373,7 @@ def run(self):
from worlds.AutoWorld import AutoWorldRegister
assert not non_apworlds - set(AutoWorldRegister.world_types), \
f"Unknown world {non_apworlds - set(AutoWorldRegister.world_types)} designated for .apworld"
folders_to_remove: typing.List[str] = []
folders_to_remove: List[str] = []
disabled_worlds_folder = "worlds_disabled"
for entry in os.listdir(disabled_worlds_folder):
if os.path.isdir(os.path.join(disabled_worlds_folder, entry)):
Expand All @@ -393,7 +394,7 @@ def run(self):
shutil.rmtree(world_directory)
shutil.copyfile("meta.yaml", self.buildfolder / "Players" / "Templates" / "meta.yaml")
try:
from maseya import z3pr
from maseya import z3pr # type: ignore[import-untyped]
except ImportError:
print("Maseya Palette Shuffle not found, skipping data files.")
else:
Expand Down Expand Up @@ -444,16 +445,16 @@ class AppImageCommand(setuptools.Command):
("app-exec=", None, "The application to run inside the image."),
("yes", "y", 'Answer "yes" to all questions.'),
]
build_folder: typing.Optional[Path]
dist_file: typing.Optional[Path]
app_dir: typing.Optional[Path]
build_folder: Optional[Path]
dist_file: Optional[Path]
app_dir: Optional[Path]
app_name: str
app_exec: typing.Optional[Path]
app_icon: typing.Optional[Path] # source file
app_exec: Optional[Path]
app_icon: Optional[Path] # source file
app_id: str # lower case name, used for icon and .desktop
yes: bool

def write_desktop(self):
def write_desktop(self) -> None:
assert self.app_dir, "Invalid app_dir"
desktop_filename = self.app_dir / f"{self.app_id}.desktop"
with open(desktop_filename, 'w', encoding="utf-8") as f:
Expand All @@ -468,7 +469,7 @@ def write_desktop(self):
)))
desktop_filename.chmod(0o755)

def write_launcher(self, default_exe: Path):
def write_launcher(self, default_exe: Path) -> None:
assert self.app_dir, "Invalid app_dir"
launcher_filename = self.app_dir / "AppRun"
with open(launcher_filename, 'w', encoding="utf-8") as f:
Expand All @@ -491,7 +492,7 @@ def write_launcher(self, default_exe: Path):
""")
launcher_filename.chmod(0o755)

def install_icon(self, src: Path, name: typing.Optional[str] = None, symlink: typing.Optional[Path] = None):
def install_icon(self, src: Path, name: Optional[str] = None, symlink: Optional[Path] = None) -> None:
assert self.app_dir, "Invalid app_dir"
try:
from PIL import Image
Expand All @@ -513,7 +514,8 @@ def install_icon(self, src: Path, name: typing.Optional[str] = None, symlink: ty
if symlink:
symlink.symlink_to(dest_file.relative_to(symlink.parent))

def initialize_options(self):
def initialize_options(self) -> None:
assert self.distribution.metadata.name
self.build_folder = None
self.app_dir = None
self.app_name = self.distribution.metadata.name
Expand All @@ -527,17 +529,22 @@ def initialize_options(self):
))
self.yes = False

def finalize_options(self):
def finalize_options(self) -> None:
assert self.build_folder
if not self.app_dir:
self.app_dir = self.build_folder.parent / "AppDir"
self.app_id = self.app_name.lower()

def run(self):
def run(self) -> None:
assert self.build_folder and self.dist_file, "Command not properly set up"
assert (
self.app_icon and self.app_id and self.app_dir and self.app_exec and self.app_name
), "AppImageCommand not properly set up"
self.dist_file.parent.mkdir(parents=True, exist_ok=True)
if self.app_dir.is_dir():
shutil.rmtree(self.app_dir)
self.app_dir.mkdir(parents=True)
opt_dir = self.app_dir / "opt" / self.distribution.metadata.name
opt_dir = self.app_dir / "opt" / self.app_name
shutil.copytree(self.build_folder, opt_dir)
root_icon = self.app_dir / f'{self.app_id}{self.app_icon.suffix}'
self.install_icon(self.app_icon, self.app_id, symlink=root_icon)
Expand All @@ -548,15 +555,15 @@ def run(self):
subprocess.call(f'ARCH={build_arch} ./appimagetool -n "{self.app_dir}" "{self.dist_file}"', shell=True)


def find_libs(*args: str) -> typing.Sequence[typing.Tuple[str, str]]:
def find_libs(*args: str) -> Sequence[Tuple[str, str]]:
"""Try to find system libraries to be included."""
if not args:
return []

arch = build_arch.replace('_', '-')
libc = 'libc6' # we currently don't support musl

def parse(line):
def parse(line: str) -> Tuple[Tuple[str, str, str], str]:
lib, path = line.strip().split(' => ')
lib, typ = lib.split(' ', 1)
for test_arch in ('x86-64', 'i386', 'aarch64'):
Expand All @@ -577,26 +584,29 @@ def parse(line):
ldconfig = shutil.which("ldconfig")
assert ldconfig, "Make sure ldconfig is in PATH"
data = subprocess.run([ldconfig, "-p"], capture_output=True, text=True).stdout.split("\n")[1:]
find_libs.cache = { # type: ignore [attr-defined]
find_libs.cache = { # type: ignore[attr-defined]
k: v for k, v in (parse(line) for line in data if "=>" in line)
}

def find_lib(lib, arch, libc):
for k, v in find_libs.cache.items():
def find_lib(lib: str, arch: str, libc: str) -> Optional[str]:
cache: Dict[Tuple[str, str, str], str] = getattr(find_libs, "cache")
for k, v in cache.items():
if k == (lib, arch, libc):
return v
for k, v, in find_libs.cache.items():
for k, v, in cache.items():
if k[0].startswith(lib) and k[1] == arch and k[2] == libc:
return v
return None

res = []
res: List[Tuple[str, str]] = []
for arg in args:
# try exact match, empty libc, empty arch, empty arch and libc
file = find_lib(arg, arch, libc)
file = file or find_lib(arg, arch, '')
file = file or find_lib(arg, '', libc)
file = file or find_lib(arg, '', '')
if not file:
raise ValueError(f"Could not find lib {arg}")
# resolve symlinks
for n in range(0, 5):
res.append((file, os.path.join('lib', os.path.basename(file))))
Expand Down
Loading