Move glfw build code into setup.py

This was on my TODO list for a long time and doing this allows the
setup.py module code to be re-used in slang.py
This commit is contained in:
Kovid Goyal
2026-06-30 09:27:27 +05:30
parent cfbae29da9
commit af61fd1b10
4 changed files with 229 additions and 231 deletions

View File

View File

@@ -2,234 +2,13 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
import json
import os
import re
import subprocess
import sys
from enum import Enum
from typing import Any, Callable, Dict, List, NamedTuple, Optional, Sequence, Tuple
_plat = sys.platform.lower()
is_linux = 'linux' in _plat
is_openbsd = 'openbsd' in _plat
base = os.path.dirname(os.path.abspath(__file__))
def null_func() -> None:
return None
class CompileKey(NamedTuple):
src: str
dest: str
class Command(NamedTuple):
desc: str
cmd: Sequence[str]
is_newer_func: Callable[[], bool]
on_success: Callable[[], None] = null_func
key: Optional[CompileKey] = None
keyfile: Optional[str] = None
class ISA(Enum):
X86 = 0x03
AMD64 = 0x3e
ARM64 = 0xb7
Other = 0x0
class BinaryArch(NamedTuple):
bits: int = 64
isa: ISA = ISA.AMD64
class CompilerType(Enum):
gcc = 'gcc'
clang = 'clang'
unknown = 'unknown'
class Env:
cc: List[str] = []
cppflags: List[str] = []
cflags: List[str] = []
ldflags: List[str] = []
library_paths: Dict[str, List[str]] = {}
ldpaths: List[str] = []
ccver: Tuple[int, int]
vcs_rev: str = ''
binary_arch: BinaryArch = BinaryArch()
native_optimizations: bool = False
primary_version: int = 0
secondary_version: int = 0
xt_version: str = ''
has_copy_file_range: bool = False
# glfw stuff
all_headers: List[str] = []
sources: List[str] = []
wayland_packagedir: str = ''
wayland_scanner: str = ''
wayland_scanner_code: str = ''
wayland_protocols: Tuple[str, ...] = ()
def __init__(
self, cc: List[str] = [], cppflags: List[str] = [], cflags: List[str] = [], ldflags: List[str] = [],
library_paths: Dict[str, List[str]] = {}, ldpaths: Optional[List[str]] = None, ccver: Tuple[int, int] = (0, 0),
vcs_rev: str = '', binary_arch: BinaryArch = BinaryArch(),
native_optimizations: bool = False,
):
self.cc, self.cppflags, self.cflags, self.ldflags, self.library_paths = cc, cppflags, cflags, ldflags, library_paths
self.ldpaths = ldpaths or []
self.ccver = ccver
self.vcs_rev = vcs_rev
self.binary_arch = binary_arch
self.native_optimizations = native_optimizations
self._cc_version_string = ''
self._compiler_type: Optional[CompilerType] = None
@property
def cc_version_string(self) -> str:
if not self._cc_version_string:
self._cc_version_string = subprocess.check_output(self.cc + ['--version']).decode()
return self._cc_version_string
@property
def compiler_type(self) -> CompilerType:
if self._compiler_type is None:
raw = self.cc_version_string
if 'Free Software Foundation' in raw:
self._compiler_type = CompilerType.gcc
elif 'clang' in raw.lower().split():
self._compiler_type = CompilerType.clang
else:
self._compiler_type = CompilerType.unknown
return self._compiler_type
def copy(self) -> 'Env':
ans = Env(self.cc, list(self.cppflags), list(self.cflags), list(self.ldflags), dict(self.library_paths), list(self.ldpaths), self.ccver)
ans.all_headers = list(self.all_headers)
ans._cc_version_string = self._cc_version_string
ans.sources = list(self.sources)
ans.wayland_packagedir = self.wayland_packagedir
ans.wayland_scanner = self.wayland_scanner
ans.wayland_scanner_code = self.wayland_scanner_code
ans.wayland_protocols = self.wayland_protocols
ans.vcs_rev = self.vcs_rev
ans.binary_arch = self.binary_arch
ans.native_optimizations = self.native_optimizations
ans.primary_version = self.primary_version
ans.secondary_version = self.secondary_version
ans.xt_version = self.xt_version
ans.has_copy_file_range = self.has_copy_file_range
return ans
def wayland_protocol_file_name(base: str, ext: str = 'c') -> str:
base = os.path.basename(base).rpartition('.')[0]
return f'wayland-{base}-client-protocol.{ext}'
def init_env(
env: Env,
pkg_config: Callable[..., List[str]],
pkg_version: Callable[[str], Tuple[int, int]],
at_least_version: Callable[..., None],
test_compile: Callable[..., Any],
module: str = 'x11'
) -> Env:
ans = env.copy()
ans.cflags.append('-fPIC')
ans.cppflags.append(f'-D_GLFW_{module.upper()}')
ans.cppflags.append('-D_GLFW_BUILD_DLL')
with open(os.path.join(base, 'source-info.json')) as f:
sinfo = json.load(f)
module_sources = list(sinfo[module]['sources'])
if module in ('x11', 'wayland'):
remove = 'null_joystick.c' if is_linux else 'linux_joystick.c'
module_sources.remove(remove)
ans.sources = sinfo['common']['sources'] + module_sources
ans.all_headers = [x for x in os.listdir(base) if x.endswith('.h')]
if module in ('x11', 'wayland'):
ans.cflags.append('-pthread')
ans.ldpaths.extend('-pthread -lm'.split())
if not is_openbsd:
ans.ldpaths.extend('-lrt -ldl'.split())
major, minor = pkg_version('xkbcommon')
if (major, minor) < (0, 5):
raise SystemExit('libxkbcommon >= 0.5 required')
if major < 1:
ans.cflags.append('-DXKB_HAS_NO_UTF32')
if module == 'x11':
for dep in 'x11 xrandr xinerama xcursor xkbcommon xkbcommon-x11 x11-xcb dbus-1'.split():
ans.cflags.extend(pkg_config(dep, '--cflags-only-I'))
ans.ldpaths.extend(pkg_config(dep, '--libs'))
elif module == 'cocoa':
ans.cppflags.append('-DGL_SILENCE_DEPRECATION')
for f_ in 'Cocoa IOKit CoreFoundation CoreVideo QuartzCore UniformTypeIdentifiers'.split():
ans.ldpaths.extend(('-framework', f_))
elif module == 'wayland':
at_least_version('wayland-protocols', *sinfo['wayland_protocols'])
ans.wayland_packagedir = os.path.abspath(pkg_config('wayland-protocols', '--variable=pkgdatadir')[0])
ans.wayland_scanner = os.path.abspath(pkg_config('wayland-scanner', '--variable=wayland_scanner')[0])
scanner_version = tuple(map(int, pkg_config('wayland-scanner', '--modversion')[0].strip().split('.')))
ans.wayland_scanner_code = 'private-code' if scanner_version >= (1, 14, 91) else 'code'
ans.wayland_protocols = tuple(sinfo[module]['protocols'])
for p in ans.wayland_protocols:
ans.sources.append(wayland_protocol_file_name(p))
ans.all_headers.append(wayland_protocol_file_name(p, 'h'))
for dep in 'wayland-client wayland-cursor xkbcommon dbus-1'.split():
ans.cflags.extend(pkg_config(dep, '--cflags-only-I'))
ans.ldpaths.extend(pkg_config(dep, '--libs'))
has_memfd_create = test_compile(env.cc, '-Werror', src='''#define _GNU_SOURCE
#include <unistd.h>
#include <sys/syscall.h>
int main(void) {
return syscall(__NR_memfd_create, "test", 0);
}''')
if has_memfd_create:
ans.cppflags.append('-DHAS_MEMFD_CREATE')
return ans
def build_wayland_protocols(
env: Env,
parallel_run: Callable[[List[Command]], None],
emphasis: Callable[[str], str],
newer: Callable[..., bool],
dest_dir: str
) -> None:
items = []
for protocol in env.wayland_protocols:
if '/' in protocol:
src = os.path.join(env.wayland_packagedir, protocol)
if not os.path.exists(src):
raise SystemExit(f'The wayland-protocols package on your system is missing the {protocol} protocol definition file')
else:
src = os.path.join(os.path.dirname(os.path.abspath(__file__)), protocol)
if not os.path.exists(src):
raise SystemExit(f'The local Wayland protocol {protocol} is missing from kitty sources')
for ext in 'hc':
dest = wayland_protocol_file_name(src, ext)
dest = os.path.join(dest_dir, dest)
if newer(dest, src):
q = 'client-header' if ext == 'h' else env.wayland_scanner_code
items.append(Command(
f'Generating {emphasis(os.path.basename(dest))} ...',
[env.wayland_scanner, q, src, dest], lambda: True))
if items:
parallel_run(items)
class Arg:

View File

@@ -404,6 +404,8 @@ def create_specialisations(sources: dict[str, SlangFile], build_dir: str, dest_d
def compile_builtin_shaders(build_dir: str, dest_dir: str, parallel_run: ParallelRun) -> None:
os.makedirs(build_dir, exist_ok=True)
os.makedirs(dest_dir, exist_ok=True)
src_dir = os.path.abspath('kitty/shaders')
source_tree = get_ordered_sources_in_tree(src_dir)
# First ensure all IR is generated

237
setup.py
View File

@@ -18,14 +18,13 @@ import tempfile
import textwrap
import time
from contextlib import suppress
from enum import Enum
from functools import lru_cache, partial
from pathlib import Path
from typing import Callable, Dict, FrozenSet, Iterable, Iterator, List, Optional, Sequence, Set, Tuple, Union, cast
from glfw import glfw
from glfw.glfw import ISA, BinaryArch, Command, CompileKey, CompilerType
from typing import Any, Callable, Dict, FrozenSet, Iterable, Iterator, List, NamedTuple, Optional, Sequence, Set, Tuple, Union, cast
src_base = os.path.dirname(os.path.abspath(__file__))
glfw_base = os.path.join(src_base, 'glfw')
setattr(sys, 'running_from_setup', True)
def check_version_info() -> None:
@@ -64,18 +63,133 @@ is_macos = 'darwin' in _plat
is_openbsd = 'openbsd' in _plat
is_freebsd = 'freebsd' in _plat
is_netbsd = 'netbsd' in _plat
is_linux = 'linux' in _plat
is_dragonflybsd = 'dragonfly' in _plat
is_bsd = is_freebsd or is_netbsd or is_dragonflybsd or is_openbsd
is_windows = sys.platform == 'win32'
is_arm = platform.processor() == 'arm' or platform.machine() in ('arm64', 'aarch64')
c_std = '' if is_openbsd else '-std=c11'
Env = glfw.Env
env = Env()
PKGCONFIG = os.environ.get('PKGCONFIG_EXE', 'pkg-config')
link_targets: List[str] = []
macos_universal_arches = ('arm64', 'x86_64') if is_arm else ('x86_64', 'arm64')
def null_func() -> None:
return None
class CompileKey(NamedTuple):
src: str
dest: str
class Command(NamedTuple):
desc: str
cmd: Sequence[str]
is_newer_func: Callable[[], bool]
on_success: Callable[[], None] = null_func
key: Optional[CompileKey] = None
keyfile: Optional[str] = None
class ISA(Enum):
X86 = 0x03
AMD64 = 0x3e
ARM64 = 0xb7
Other = 0x0
class BinaryArch(NamedTuple):
bits: int = 64
isa: ISA = ISA.AMD64
class CompilerType(Enum):
gcc = 'gcc'
clang = 'clang'
unknown = 'unknown'
class Env:
cc: List[str] = []
cppflags: List[str] = []
cflags: List[str] = []
ldflags: List[str] = []
library_paths: Dict[str, List[str]] = {}
ldpaths: List[str] = []
ccver: Tuple[int, int]
vcs_rev: str = ''
binary_arch: BinaryArch = BinaryArch()
native_optimizations: bool = False
primary_version: int = 0
secondary_version: int = 0
xt_version: str = ''
has_copy_file_range: bool = False
# glfw stuff
all_headers: List[str] = []
sources: List[str] = []
wayland_packagedir: str = ''
wayland_scanner: str = ''
wayland_scanner_code: str = ''
wayland_protocols: Tuple[str, ...] = ()
def __init__(
self, cc: List[str] = [], cppflags: List[str] = [], cflags: List[str] = [], ldflags: List[str] = [],
library_paths: Dict[str, List[str]] = {}, ldpaths: Optional[List[str]] = None, ccver: Tuple[int, int] = (0, 0),
vcs_rev: str = '', binary_arch: BinaryArch = BinaryArch(),
native_optimizations: bool = False,
):
self.cc, self.cppflags, self.cflags, self.ldflags, self.library_paths = cc, cppflags, cflags, ldflags, library_paths
self.ldpaths = ldpaths or []
self.ccver = ccver
self.vcs_rev = vcs_rev
self.binary_arch = binary_arch
self.native_optimizations = native_optimizations
self._cc_version_string = ''
self._compiler_type: Optional[CompilerType] = None
@property
def cc_version_string(self) -> str:
if not self._cc_version_string:
self._cc_version_string = subprocess.check_output(self.cc + ['--version']).decode()
return self._cc_version_string
@property
def compiler_type(self) -> CompilerType:
if self._compiler_type is None:
raw = self.cc_version_string
if 'Free Software Foundation' in raw:
self._compiler_type = CompilerType.gcc
elif 'clang' in raw.lower().split():
self._compiler_type = CompilerType.clang
else:
self._compiler_type = CompilerType.unknown
return self._compiler_type
def copy(self) -> 'Env':
ans = Env(self.cc, list(self.cppflags), list(self.cflags), list(self.ldflags), dict(self.library_paths), list(self.ldpaths), self.ccver)
ans.all_headers = list(self.all_headers)
ans._cc_version_string = self._cc_version_string
ans.sources = list(self.sources)
ans.wayland_packagedir = self.wayland_packagedir
ans.wayland_scanner = self.wayland_scanner
ans.wayland_scanner_code = self.wayland_scanner_code
ans.wayland_protocols = self.wayland_protocols
ans.vcs_rev = self.vcs_rev
ans.binary_arch = self.binary_arch
ans.native_optimizations = self.native_optimizations
ans.primary_version = self.primary_version
ans.secondary_version = self.secondary_version
ans.xt_version = self.xt_version
ans.has_copy_file_range = self.has_copy_file_range
return ans
env = Env()
def LinkKey(output: str) -> CompileKey:
return CompileKey('', output)
@@ -1037,11 +1151,114 @@ def find_c_files() -> Tuple[List[str], List[str]]:
return ans, headers
def wayland_protocol_file_name(base: str, ext: str = 'c') -> str:
base = os.path.basename(base).rpartition('.')[0]
return f'wayland-{base}-client-protocol.{ext}'
def glfw_init_env(
env: Env,
pkg_config: Callable[..., List[str]],
pkg_version: Callable[[str], Tuple[int, int]],
at_least_version: Callable[..., None],
test_compile: Callable[..., Any],
module: str = 'x11'
) -> Env:
ans = env.copy()
ans.cflags.append('-fPIC')
ans.cppflags.append(f'-D_GLFW_{module.upper()}')
ans.cppflags.append('-D_GLFW_BUILD_DLL')
with open(os.path.join(glfw_base, 'source-info.json')) as f:
sinfo = json.load(f)
module_sources = list(sinfo[module]['sources'])
if module in ('x11', 'wayland'):
remove = 'null_joystick.c' if is_linux else 'linux_joystick.c'
module_sources.remove(remove)
ans.sources = sinfo['common']['sources'] + module_sources
ans.all_headers = [x for x in os.listdir(glfw_base) if x.endswith('.h')]
if module in ('x11', 'wayland'):
ans.cflags.append('-pthread')
ans.ldpaths.extend('-pthread -lm'.split())
if not is_openbsd:
ans.ldpaths.extend('-lrt -ldl'.split())
major, minor = pkg_version('xkbcommon')
if (major, minor) < (0, 5):
raise SystemExit('libxkbcommon >= 0.5 required')
if major < 1:
ans.cflags.append('-DXKB_HAS_NO_UTF32')
if module == 'x11':
for dep in 'x11 xrandr xinerama xcursor xkbcommon xkbcommon-x11 x11-xcb dbus-1'.split():
ans.cflags.extend(pkg_config(dep, '--cflags-only-I'))
ans.ldpaths.extend(pkg_config(dep, '--libs'))
elif module == 'cocoa':
ans.cppflags.append('-DGL_SILENCE_DEPRECATION')
for f_ in 'Cocoa IOKit CoreFoundation CoreVideo QuartzCore UniformTypeIdentifiers'.split():
ans.ldpaths.extend(('-framework', f_))
elif module == 'wayland':
at_least_version('wayland-protocols', *sinfo['wayland_protocols'])
ans.wayland_packagedir = os.path.abspath(pkg_config('wayland-protocols', '--variable=pkgdatadir')[0])
ans.wayland_scanner = os.path.abspath(pkg_config('wayland-scanner', '--variable=wayland_scanner')[0])
scanner_version = tuple(map(int, pkg_config('wayland-scanner', '--modversion')[0].strip().split('.')))
ans.wayland_scanner_code = 'private-code' if scanner_version >= (1, 14, 91) else 'code'
ans.wayland_protocols = tuple(sinfo[module]['protocols'])
for p in ans.wayland_protocols:
ans.sources.append(wayland_protocol_file_name(p))
ans.all_headers.append(wayland_protocol_file_name(p, 'h'))
for dep in 'wayland-client wayland-cursor xkbcommon dbus-1'.split():
ans.cflags.extend(pkg_config(dep, '--cflags-only-I'))
ans.ldpaths.extend(pkg_config(dep, '--libs'))
has_memfd_create = test_compile(env.cc, '-Werror', src='''#define _GNU_SOURCE
#include <unistd.h>
#include <sys/syscall.h>
int main(void) {
return syscall(__NR_memfd_create, "test", 0);
}''')
if has_memfd_create:
ans.cppflags.append('-DHAS_MEMFD_CREATE')
return ans
def build_wayland_protocols(
env: Env,
parallel_run: Callable[[List[Command]], None],
emphasis: Callable[[str], str],
newer: Callable[..., bool],
dest_dir: str
) -> None:
items = []
for protocol in env.wayland_protocols:
if '/' in protocol:
src = os.path.join(env.wayland_packagedir, protocol)
if not os.path.exists(src):
raise SystemExit(f'The wayland-protocols package on your system is missing the {protocol} protocol definition file')
else:
src = os.path.join(glfw_base, protocol)
if not os.path.exists(src):
raise SystemExit(f'The local Wayland protocol {protocol} is missing from kitty sources')
for ext in 'hc':
dest = wayland_protocol_file_name(src, ext)
dest = os.path.join(dest_dir, dest)
if newer(dest, src):
q = 'client-header' if ext == 'h' else env.wayland_scanner_code
items.append(Command(
f'Generating {emphasis(os.path.basename(dest))} ...',
[env.wayland_scanner, q, src, dest], lambda: True))
if items:
parallel_run(items)
def compile_glfw(compilation_database: CompilationDatabase, build_dsym: bool = False) -> None:
modules = 'cocoa' if is_macos else 'x11 wayland'
for module in modules.split():
try:
genv = glfw.init_env(env, pkg_config, pkg_version, at_least_version, test_compile, module)
genv = glfw_init_env(env, pkg_config, pkg_version, at_least_version, test_compile, module)
except SystemExit as err:
if module != 'wayland':
raise
@@ -1052,7 +1269,7 @@ def compile_glfw(compilation_database: CompilationDatabase, build_dsym: bool = F
all_headers = [os.path.join('glfw', x) for x in genv.all_headers]
if module == 'wayland':
try:
glfw.build_wayland_protocols(genv, parallel_run, emphasis, newer, 'glfw')
build_wayland_protocols(genv, parallel_run, emphasis, newer, 'glfw')
except SystemExit as err:
print(err, file=sys.stderr)
print(error('Disabling building of wayland backend'), file=sys.stderr)
@@ -1893,8 +2110,7 @@ def package(args: Options, bundle_type: str, do_build_all: bool = True) -> None:
shutil.copy2('logo/beam-cursor@2x.png', os.path.join(libdir, 'logo'))
shutil.copytree('shell-integration', os.path.join(libdir, 'shell-integration'), dirs_exist_ok=True)
shutil.copytree('fonts', os.path.join(libdir, 'fonts'), dirs_exist_ok=True)
shutil.copytree('shaders', os.path.join(libdir, 'shaders'), dirs_exist_ok=True)
allowed_extensions = frozenset('py glsl so'.split())
allowed_extensions = frozenset('py slang glsl so'.split())
def src_ignore(parent: str, entries: Iterable[str]) -> List[str]:
return [
@@ -1943,6 +2159,7 @@ def package(args: Options, bundle_type: str, do_build_all: bool = True) -> None:
os.chmod(path, 0o755 if should_be_executable(path) else 0o644)
if not for_freeze and not bundle_type.startswith('macos-'):
build_static_kittens(args, launcher_dir=launcher_dir)
shutil.copytree('shaders', os.path.join(libdir, 'shaders'), dirs_exist_ok=True)
if not is_macos and not is_windows:
create_linux_bundle_gunk(ddir, args)