mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-19 21:37:57 +08:00
730 lines
28 KiB
Python
Executable File
730 lines
28 KiB
Python
Executable File
# Released under the MIT License. See LICENSE for details.
|
|
#
|
|
"""General project related functionality."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING
|
|
from dataclasses import dataclass
|
|
|
|
from efrotools.project import getprojectconfig, getlocalconfig
|
|
from efro.error import CleanError
|
|
from efro.terminal import Clr
|
|
|
|
if TYPE_CHECKING:
|
|
from batools.featureset import FeatureSet
|
|
|
|
|
|
def project_centric_path(projroot: str, path: str) -> str:
|
|
"""Convert a CWD-relative path to a project-relative one."""
|
|
abspath = os.path.abspath(path)
|
|
if abspath == projroot:
|
|
return '.'
|
|
projprefix = f'{projroot}/'
|
|
if not abspath.startswith(projprefix):
|
|
raise RuntimeError(
|
|
f'Path "{abspath}" is not under project root "{projprefix}"'
|
|
)
|
|
return abspath[len(projprefix) :]
|
|
|
|
|
|
@dataclass
|
|
class _LineChange:
|
|
"""A change applying to a particular line in a file."""
|
|
|
|
line_number: int
|
|
expected: str
|
|
can_auto_update: bool
|
|
|
|
|
|
class ProjectUpdater:
|
|
"""Context for an project-updater run."""
|
|
|
|
def __init__(
|
|
self,
|
|
projroot: str,
|
|
*,
|
|
check: bool,
|
|
fix: bool,
|
|
empty: bool = False,
|
|
projname: str = 'BallisticaKit',
|
|
) -> None:
|
|
self.projname = projname
|
|
self.projroot = os.path.abspath(projroot)
|
|
self.check = check
|
|
self.fix = fix
|
|
|
|
# 'fix' implies making changes and 'check' implies no changes.
|
|
if fix and check:
|
|
raise RuntimeError('fix and check cannot both be enabled')
|
|
|
|
# We behave a bit differently in the public repo.
|
|
self.public: bool = getprojectconfig(Path(projroot)).get(
|
|
'public', False
|
|
)
|
|
assert isinstance(self.public, bool)
|
|
|
|
self._source_files: list[str] | None = None
|
|
self._header_files: list[str] | None = None
|
|
|
|
# Set of files this updater instance will update.
|
|
# Add stuff here as desired before calling run().
|
|
# The associated value can be input data for the file.
|
|
# If None, the existing file will be read from disk.
|
|
self._enqueued_updates: dict[str, str | None] = {}
|
|
|
|
# Individual line corrections made in _fix mode.
|
|
self._line_corrections: dict[str, list[_LineChange]] = {}
|
|
|
|
self._can_generate_files = False
|
|
|
|
# All files generated by going through updates. Note that
|
|
# this can include files not explicitly requested in
|
|
# updates (manifest files or other side-effect files).
|
|
self._generated_files: dict[str, str] = {}
|
|
|
|
# Cached feature-set list for any functionality/tools that might
|
|
# need it.
|
|
self._feature_sets: dict[str, FeatureSet] | None = None
|
|
|
|
self.license_line_checks = bool(
|
|
getlocalconfig(Path(projroot)).get('license_line_checks', True)
|
|
)
|
|
|
|
self._internal_source_dirs: set[str] | None = None
|
|
self._internal_source_files: set[str] | None = None
|
|
|
|
# Whether to run various checks across project files. This can
|
|
# be turned off to speed things up when updating a focused set
|
|
# of files.
|
|
self.run_file_checks = True
|
|
|
|
# For 'empty' mode we disable all default stuff and only do
|
|
# exactly what is requested of us.
|
|
if empty:
|
|
self.run_file_checks = False
|
|
else:
|
|
# Schedule updates for all the things in normal mode.
|
|
self._update_meta_makefile()
|
|
self._update_resources_makefile()
|
|
self._update_assets_makefile()
|
|
self._update_top_level_makefile()
|
|
self._update_cmake_files()
|
|
self._update_visual_studio_projects()
|
|
self._update_xcode_projects()
|
|
self._update_app_module()
|
|
|
|
@property
|
|
def source_files(self) -> list[str]:
|
|
"""Return project source files."""
|
|
assert self._source_files is not None
|
|
return self._source_files
|
|
|
|
@property
|
|
def header_files(self) -> list[str]:
|
|
"""Return project header files."""
|
|
assert self._header_files is not None
|
|
return self._header_files
|
|
|
|
@property
|
|
def feature_sets(self) -> dict[str, FeatureSet]:
|
|
"""Cached list of project feature-sets."""
|
|
if self._feature_sets is None:
|
|
from batools.featureset import FeatureSet
|
|
|
|
self._feature_sets = {
|
|
f.name: f for f in FeatureSet.get_all_for_project(self.projroot)
|
|
}
|
|
return self._feature_sets
|
|
|
|
def enqueue_update(self, path: str, data: str | None = None) -> None:
|
|
"""Add an update to the queue."""
|
|
self._enqueued_updates[path] = data
|
|
|
|
def run(self) -> None:
|
|
"""Do the thing."""
|
|
self.prepare_to_generate()
|
|
start_updates = self._enqueued_updates.copy()
|
|
|
|
# Generate all files we've been asked to.
|
|
for path in self._enqueued_updates:
|
|
self.generate_file(path)
|
|
|
|
# Run some lovely checks.
|
|
if self.run_file_checks:
|
|
from batools.project import _checks
|
|
|
|
_checks.check_makefiles(self)
|
|
_checks.check_python_files(self)
|
|
_checks.check_sync_states(self)
|
|
_checks.check_misc(self)
|
|
_checks.check_source_files(self)
|
|
_checks.check_headers(self)
|
|
|
|
# Make sure nobody is changing this while processing.
|
|
self._can_generate_files = False
|
|
assert start_updates == self._enqueued_updates
|
|
|
|
# If we're all good to here, do the actual writes we set up above.
|
|
self._apply_line_changes()
|
|
self._apply_file_changes()
|
|
|
|
def prepare_to_generate(self) -> None:
|
|
"""Prepare"""
|
|
# Make sure we're operating from a project root.
|
|
if not os.path.isdir(
|
|
os.path.join(self.projroot, 'config')
|
|
) or not os.path.isdir(os.path.join(self.projroot, 'tools')):
|
|
raise RuntimeError(
|
|
f"ProjectUpdater projroot '{self.projroot}' is not valid."
|
|
)
|
|
|
|
self._find_sources_and_headers(
|
|
os.path.join(self.projroot, 'src/ballistica')
|
|
)
|
|
|
|
self._can_generate_files = True
|
|
|
|
def _get_internal_source_files(self) -> set[str]:
|
|
# Fetch/calc just once and cache results.
|
|
if self._internal_source_files is None:
|
|
sources: list[str]
|
|
if self.public:
|
|
sources = []
|
|
else:
|
|
sources = getprojectconfig(Path(self.projroot)).get(
|
|
'internal_source_files', []
|
|
)
|
|
if not isinstance(sources, list):
|
|
raise CleanError(
|
|
f'Expected list for internal_source_files;'
|
|
f' got {type(sources)}'
|
|
)
|
|
self._internal_source_files = set(sources)
|
|
return self._internal_source_files
|
|
|
|
def _get_internal_source_dirs(self) -> set[str]:
|
|
# Fetch/calc just once and cache results.
|
|
if self._internal_source_dirs is None:
|
|
sources: list[str]
|
|
if self.public:
|
|
sources = []
|
|
else:
|
|
sources = getprojectconfig(Path(self.projroot)).get(
|
|
'internal_source_dirs', []
|
|
)
|
|
if not isinstance(sources, list):
|
|
raise CleanError(
|
|
f'Expected list for internal_source_dirs;'
|
|
f' got {type(sources)}'
|
|
)
|
|
self._internal_source_dirs = set(sources)
|
|
return self._internal_source_dirs
|
|
|
|
def _apply_file_changes(self) -> None:
|
|
# Now write out any project files that have changed
|
|
# (or error if we're in check mode).
|
|
unchanged_file_count = 0
|
|
for fname, fcode in self._generated_files.items():
|
|
f_path_abs = os.path.join(self.projroot, fname)
|
|
# Allow for line ending changes by git?...
|
|
fcodefin = fcode.replace('\r\n', '\n')
|
|
f_orig: str | None
|
|
if os.path.exists(f_path_abs):
|
|
with open(f_path_abs, 'r', encoding='utf-8') as infile:
|
|
f_orig = infile.read()
|
|
else:
|
|
f_orig = None
|
|
if f_orig == fcodefin:
|
|
unchanged_file_count += 1
|
|
else:
|
|
if self.check:
|
|
# Dump the generated and print a command to diff it
|
|
# against the original. This can be useful to
|
|
# diagnose non-deterministic generation issues.
|
|
errfile = os.path.join(
|
|
self.projroot, 'build/project_check_error_file'
|
|
)
|
|
os.makedirs(os.path.dirname(errfile), exist_ok=True)
|
|
with open(errfile, 'w', encoding='utf-8') as outfile:
|
|
outfile.write(fcodefin)
|
|
path1 = f_path_abs
|
|
path2 = errfile
|
|
raise CleanError(
|
|
f"Found out-of-date project file: '{fname}'.\n"
|
|
'To see what would change, run:\n'
|
|
f" diff '{path1}' '{path2}'\n"
|
|
)
|
|
|
|
print(f'{Clr.BLU}Writing project file: {fname}{Clr.RST}')
|
|
with open(f_path_abs, 'w', encoding='utf-8') as outfile:
|
|
outfile.write(fcode)
|
|
if unchanged_file_count > 0:
|
|
print(f'{unchanged_file_count} project files are up to date.')
|
|
|
|
def _apply_line_changes(self) -> None:
|
|
# Build a flat list of entries that can and can-not be auto
|
|
# applied.
|
|
manual_changes: list[tuple[str, _LineChange]] = []
|
|
auto_changes: list[tuple[str, _LineChange]] = []
|
|
for fname, entries in self._line_corrections.items():
|
|
for entry in entries:
|
|
if entry.can_auto_update:
|
|
auto_changes.append((fname, entry))
|
|
else:
|
|
manual_changes.append((fname, entry))
|
|
|
|
# If there are any manual-only entries, list then and bail.
|
|
# (Don't wanna allow auto-apply unless it fixes everything)
|
|
if manual_changes:
|
|
print(
|
|
f'{Clr.RED}Found erroneous lines '
|
|
f'requiring manual correction:{Clr.RST}'
|
|
)
|
|
for change in manual_changes:
|
|
print(
|
|
f'{Clr.RED}{change[0]}:{change[1].line_number + 1}:'
|
|
f' Expected line to be:\n {change[1].expected}{Clr.RST}'
|
|
)
|
|
|
|
raise CleanError()
|
|
|
|
# Now, if we've got auto entries, either list or auto-correct them.
|
|
if auto_changes:
|
|
if not self.fix:
|
|
for i, change in enumerate(auto_changes):
|
|
print(
|
|
f'{Clr.RED}#{i}:'
|
|
f' {change[0]}:{change[1].line_number+1}:'
|
|
f'{Clr.RST}'
|
|
)
|
|
print(
|
|
f'{Clr.RED} Expected "{change[1].expected}"{Clr.RST}'
|
|
)
|
|
with open(
|
|
os.path.join(self.projroot, change[0]), encoding='utf-8'
|
|
) as infile:
|
|
lines = infile.read().splitlines()
|
|
line = lines[change[1].line_number]
|
|
print(f'{Clr.RED} Found "{line}"{Clr.RST}')
|
|
raise CleanError(
|
|
f'All {len(auto_changes)} errors are'
|
|
f' auto-fixable; run tools/pcommand update_project'
|
|
f' --fix to apply corrections.'
|
|
)
|
|
|
|
for i, change in enumerate(auto_changes):
|
|
print(
|
|
f'{Clr.BLU}{Clr.BLD}Correcting'
|
|
f' {change[0]} line {change[1].line_number+1}{Clr.RST}'
|
|
)
|
|
with open(
|
|
os.path.join(self.projroot, change[0]), encoding='utf-8'
|
|
) as infile:
|
|
lines = infile.read().splitlines()
|
|
lines[change[1].line_number] = change[1].expected
|
|
with open(
|
|
os.path.join(self.projroot, change[0]),
|
|
'w',
|
|
encoding='utf-8',
|
|
) as outfile:
|
|
outfile.write('\n'.join(lines) + '\n')
|
|
|
|
# If there were no issues whatsoever, note that.
|
|
if not manual_changes and not auto_changes:
|
|
fcount = len(self.header_files) + len(self.source_files)
|
|
print(f'No issues found in {fcount} source files.')
|
|
|
|
def add_line_correction(
|
|
self,
|
|
filename: str,
|
|
line_number: int,
|
|
expected: str,
|
|
can_auto_update: bool,
|
|
) -> None:
|
|
"""Add a correction that the updater can optionally perform."""
|
|
# No longer allowing negatives here since they don't show up
|
|
# nicely in correction list.
|
|
assert line_number >= 0
|
|
self._line_corrections.setdefault(filename, []).append(
|
|
_LineChange(
|
|
line_number=line_number,
|
|
expected=expected,
|
|
can_auto_update=can_auto_update,
|
|
)
|
|
)
|
|
|
|
def generate_file(self, path: str) -> str:
|
|
"""Generate/return the contents for the file at the given path."""
|
|
# pylint: disable=too-many-branches
|
|
|
|
if not self._can_generate_files:
|
|
raise RuntimeError('Generate cannot be called right now.')
|
|
|
|
if path not in self._generated_files:
|
|
# First we need input data. If the user provided it explicitly,
|
|
# go with theirs. Otherwise load the existing file from disk.
|
|
existing_data = self._enqueued_updates.get(path)
|
|
if existing_data is None:
|
|
with open(
|
|
os.path.join(self.projroot, path), encoding='utf-8'
|
|
) as infile:
|
|
existing_data = infile.read()
|
|
# Dispatch to generator methods depending on extension/etc.
|
|
if path.endswith('/project.pbxproj'):
|
|
self._generate_xcode_project(path, existing_data)
|
|
elif path.endswith('.vcxproj.filters'):
|
|
self._generate_visual_studio_project_filters(
|
|
path, existing_data
|
|
)
|
|
elif path.endswith('.vcxproj'):
|
|
self._generate_visual_studio_project(path, existing_data)
|
|
elif path.endswith('CMakeLists.txt'):
|
|
self._generate_cmake_file(path, existing_data)
|
|
elif path == 'Makefile':
|
|
self._generate_top_level_makefile(path, existing_data)
|
|
elif path == 'src/assets/Makefile':
|
|
self._generate_assets_makefile(path, existing_data)
|
|
elif path.startswith('src/assets/.asset_manifest_public'):
|
|
# These are always generated as a side-effect of the
|
|
# assets Makefile.
|
|
self.generate_file('src/assets/Makefile')
|
|
elif path.startswith('src/assets/.asset_manifest_private'):
|
|
if self.public:
|
|
# In public repos these are just pulled through as-is
|
|
# from the source project.
|
|
self._generate_passthrough_file(path, existing_data)
|
|
else:
|
|
# In private repos, these are generated as a side-effect
|
|
# of the assets Makefile.
|
|
self.generate_file('src/assets/Makefile')
|
|
|
|
elif path == 'src/resources/Makefile':
|
|
self._generate_resources_makefile(path, existing_data)
|
|
elif path == 'src/meta/Makefile':
|
|
self._generate_meta_makefile(existing_data)
|
|
elif path == 'src/assets/ba_data/python/babase/_app.py':
|
|
self._generate_app_module(path, existing_data)
|
|
elif path.startswith('src/meta/.meta_manifest_'):
|
|
# These are always generated as a side-effect of the
|
|
# meta Makefile.
|
|
self.generate_file('src/meta/Makefile')
|
|
assert path in self._generated_files
|
|
else:
|
|
raise RuntimeError(
|
|
f"No known formula to create project file: '{path}'."
|
|
)
|
|
|
|
assert path in self._generated_files
|
|
return self._generated_files[path]
|
|
|
|
def _update_app_module(self) -> None:
|
|
self.enqueue_update('src/assets/ba_data/python/babase/_app.py')
|
|
|
|
def _update_xcode_projects(self) -> None:
|
|
# from batools.xcode import update_xcode_project
|
|
|
|
for projpath in [
|
|
'ballisticakit-xcode/BallisticaKit.xcodeproj/project.pbxproj'
|
|
]:
|
|
# These currently aren't bundled in public.
|
|
if self.public:
|
|
assert not os.path.exists(projpath)
|
|
continue
|
|
|
|
self.enqueue_update(projpath)
|
|
|
|
def _generate_xcode_project(self, path: str, existing_data: str) -> None:
|
|
from batools.xcodeproject import update_xcode_project
|
|
|
|
all_files = sorted(
|
|
[f'ballistica{p}' for p in (self.source_files + self.header_files)]
|
|
)
|
|
|
|
# We have .pbxproj; this wants .xcodeproj above it.
|
|
# Should probably change that as its confusing...
|
|
assert path.endswith('.pbxproj')
|
|
self._generated_files[path] = update_xcode_project(
|
|
self.projroot,
|
|
os.path.dirname(path),
|
|
existing_data,
|
|
all_files,
|
|
projname=self.projname,
|
|
)
|
|
|
|
def _update_visual_studio_project(self, basename: str) -> None:
|
|
fname = (
|
|
f'ballisticakit-windows/{basename}/'
|
|
f'BallisticaKit{basename}.vcxproj'
|
|
)
|
|
self.enqueue_update(f'{fname}.filters')
|
|
self.enqueue_update(fname)
|
|
|
|
def _generate_visual_studio_project(
|
|
self, fname: str, existing_data: str
|
|
) -> None:
|
|
lines = existing_data.splitlines()
|
|
|
|
src_root = '..\\..\\src'
|
|
|
|
public_project = 'Plus' not in os.path.basename(fname)
|
|
|
|
all_files = sorted(
|
|
[
|
|
f
|
|
for f in (self.source_files + self.header_files)
|
|
if not f.endswith('.m')
|
|
and not f.endswith('.mm')
|
|
and not f.endswith('.c')
|
|
and not f.endswith('.swift')
|
|
and self._is_public_source_file(f) == public_project
|
|
]
|
|
)
|
|
|
|
# Find the ItemGroup containing stdafx.cpp. This is where we'll
|
|
# dump our stuff.
|
|
index = lines.index(' <ClCompile Include="stdafx.cpp">')
|
|
begin_index = end_index = index
|
|
while lines[begin_index] != ' <ItemGroup>':
|
|
begin_index -= 1
|
|
while lines[end_index] != ' </ItemGroup>':
|
|
end_index += 1
|
|
group_lines = lines[begin_index + 1 : end_index]
|
|
|
|
# Strip out any existing files from src/ballistica.
|
|
group_lines = [
|
|
l for l in group_lines if src_root + '\\ballistica\\' not in l
|
|
]
|
|
|
|
# Now add in our own.
|
|
# Note: we can't use C files in this build at the moment; breaks
|
|
# precompiled header stuff. (shouldn't be a problem though).
|
|
group_lines = [
|
|
' <'
|
|
+ ('ClInclude' if src.endswith('.h') else 'ClCompile')
|
|
+ ' Include="'
|
|
+ src_root
|
|
+ '\\ballistica'
|
|
+ src.replace('/', '\\')
|
|
+ '" />'
|
|
for src in all_files
|
|
] + group_lines
|
|
|
|
filtered = lines[: begin_index + 1] + group_lines + lines[end_index:]
|
|
out = '\r\n'.join(filtered) + '\r\n'
|
|
self._generated_files[fname] = out
|
|
|
|
def _generate_visual_studio_project_filters(
|
|
self, fname: str, existing_data: str
|
|
) -> None:
|
|
del existing_data # Unused.
|
|
assert fname.endswith('.filters')
|
|
# We rely on the generated project file itself.
|
|
project = self.generate_file(fname.removesuffix('.filters'))
|
|
lines_in = project.splitlines()
|
|
src_root = '..\\..\\src'
|
|
filterpaths: set[str] = set()
|
|
filterlines: list[str] = [
|
|
'<?xml version="1.0" encoding="utf-8"?>',
|
|
'<Project ToolsVersion="4.0"'
|
|
' xmlns="http://schemas.microsoft.com/developer/msbuild/2003">',
|
|
' <ItemGroup>',
|
|
]
|
|
sourcelines = [l for l in lines_in if 'Include="' + src_root in l]
|
|
for line in sourcelines:
|
|
entrytype = line.strip().split()[0][1:]
|
|
path = line.split('"')[1]
|
|
filterlines.append(' <' + entrytype + ' Include="' + path + '">')
|
|
|
|
# If we have a dir foo/bar/eep we need to create filters for
|
|
# each of foo, foo/bar, and foo/bar/eep
|
|
splits = path[len(src_root) :].split('\\')
|
|
splits = [s for s in splits if s != '']
|
|
splits = splits[:-1]
|
|
for i in range(len(splits)):
|
|
filterpaths.add('\\'.join(splits[: (i + 1)]))
|
|
filterlines.append(
|
|
' <Filter>' + '\\'.join(splits) + '</Filter>'
|
|
)
|
|
filterlines.append(' </' + entrytype + '>')
|
|
filterlines += [
|
|
' </ItemGroup>',
|
|
' <ItemGroup>',
|
|
]
|
|
for filterpath in sorted(filterpaths):
|
|
filterlines.append(' <Filter Include="' + filterpath + '" />')
|
|
filterlines += [
|
|
' </ItemGroup>',
|
|
'</Project>',
|
|
]
|
|
self._generated_files[fname] = '\r\n'.join(filterlines) + '\r\n'
|
|
|
|
def _update_visual_studio_projects(self) -> None:
|
|
self._update_visual_studio_project('Generic')
|
|
self._update_visual_studio_project('Headless')
|
|
if not self.public:
|
|
self._update_visual_studio_project('GenericPlus')
|
|
self._update_visual_studio_project('HeadlessPlus')
|
|
self._update_visual_studio_project('Oculus')
|
|
self._update_visual_studio_project('OculusPlus')
|
|
|
|
def _is_public_source_file(self, filename: str) -> bool:
|
|
assert filename.startswith('/')
|
|
filename = f'src/ballistica{filename}'
|
|
|
|
# If its under any of our internal source dirs, make it internal.
|
|
for srcdir in self._get_internal_source_dirs():
|
|
assert not srcdir.startswith('/')
|
|
assert not srcdir.endswith('/')
|
|
if filename.startswith(f'{srcdir}/'):
|
|
return False
|
|
|
|
# If its specifically listed as an internal file, make it internal.
|
|
return filename not in self._get_internal_source_files()
|
|
|
|
def _generate_cmake_file(self, fname: str, existing_data: str) -> None:
|
|
lines = existing_data.splitlines()
|
|
|
|
for section in ['PUBLIC', 'PRIVATE']:
|
|
# Public repo has no private section.
|
|
if self.public and section == 'PRIVATE':
|
|
continue
|
|
|
|
auto_start = lines.index(
|
|
f' # AUTOGENERATED_{section}_BEGIN (this section'
|
|
f' is managed by the "update_project" tool)'
|
|
)
|
|
auto_end = lines.index(f' # AUTOGENERATED_{section}_END')
|
|
our_lines = [
|
|
' ${BA_SRC_ROOT}/ballistica' + f
|
|
for f in sorted(self.source_files + self.header_files)
|
|
if not f.endswith('.mm')
|
|
and not f.endswith('.m')
|
|
and not f.endswith('.swift')
|
|
and self._is_public_source_file(f) == (section == 'PUBLIC')
|
|
]
|
|
lines = lines[: auto_start + 1] + our_lines + lines[auto_end:]
|
|
|
|
self._generated_files[fname] = '\n'.join(lines) + '\n'
|
|
|
|
def _update_cmake_files(self) -> None:
|
|
# Our regular cmake build.
|
|
self.enqueue_update('ballisticakit-cmake/CMakeLists.txt')
|
|
|
|
# Our Android cmake build (Currently not included in public).
|
|
fname = (
|
|
'ballisticakit-android/BallisticaKit/src/main/cpp/CMakeLists.txt'
|
|
)
|
|
if not self.public:
|
|
self.enqueue_update(fname)
|
|
else:
|
|
# So we don't forget to turn this on once added.
|
|
assert not os.path.exists(fname)
|
|
|
|
def _find_sources_and_headers(self, scan_dir: str) -> None:
|
|
src_files = set()
|
|
header_files = set()
|
|
exts = ['.c', '.cc', '.cpp', '.cxx', '.m', '.mm', '.swift']
|
|
header_exts = ['.h']
|
|
|
|
# Gather all sources and headers.
|
|
# HMMM: Ideally we should use
|
|
# efrotools.code.get_code_filenames() here (though we return
|
|
# things relative to the scan-dir which could throw things off).
|
|
for root, _dirs, files in os.walk(scan_dir):
|
|
for ftst in files:
|
|
if any(ftst.endswith(ext) for ext in exts):
|
|
src_files.add(os.path.join(root, ftst)[len(scan_dir) :])
|
|
if any(ftst.endswith(ext) for ext in header_exts):
|
|
header_files.add(os.path.join(root, ftst)[len(scan_dir) :])
|
|
|
|
# IMPORTANT - exclude generated files.
|
|
# For now these just consist of headers so its ok to completely
|
|
# ignore their existence here, but at some point if we start
|
|
# generating .cc files that need to be compiled we'll have to
|
|
# ask the meta system which files it *will* be generating and
|
|
# add THAT list (not what we see on disk) to projects.
|
|
self._source_files = sorted(s for s in src_files if '/mgen/' not in s)
|
|
self._header_files = sorted(
|
|
h for h in header_files if '/mgen/' not in h
|
|
)
|
|
|
|
def _update_assets_makefile(self) -> None:
|
|
self.enqueue_update('src/assets/Makefile')
|
|
|
|
def _generate_assets_makefile(self, path: str, existing_data: str) -> None:
|
|
from batools.assetsmakefile import generate_assets_makefile
|
|
|
|
# We need to know what files meta will be creating (since they
|
|
# can be asset sources).
|
|
meta_manifests: dict[str, str] = {}
|
|
for mantype in ['public', 'private']:
|
|
manifest_file_name = f'src/meta/.meta_manifest_{mantype}.json'
|
|
meta_manifests[manifest_file_name] = self.generate_file(
|
|
manifest_file_name
|
|
)
|
|
|
|
# Special case; the app module file in the base feature set
|
|
# is created/updated here as a project file. It may or may not
|
|
# exist on disk, but we want to ignore it if it does and add it
|
|
# explicitly similarly to meta-manifests.
|
|
if 'base' in self.feature_sets:
|
|
explicit_sources = {'src/assets/ba_data/python/babase/_app.py'}
|
|
else:
|
|
explicit_sources = set()
|
|
|
|
outfiles = generate_assets_makefile(
|
|
self.projroot, path, existing_data, meta_manifests, explicit_sources
|
|
)
|
|
|
|
for out_path, out_contents in outfiles.items():
|
|
self._generated_files[out_path] = out_contents
|
|
|
|
def _update_top_level_makefile(self) -> None:
|
|
self.enqueue_update('Makefile')
|
|
|
|
def _generate_top_level_makefile(
|
|
self, path: str, existing_data: str
|
|
) -> None:
|
|
from batools.toplevelmakefile import generate_top_level_makefile
|
|
|
|
self._generated_files[path] = generate_top_level_makefile(
|
|
self.projroot, existing_data
|
|
)
|
|
|
|
def _generate_app_module(self, path: str, existing_data: str) -> None:
|
|
from batools.appmodule import generate_app_module
|
|
|
|
self._generated_files[path] = generate_app_module(
|
|
self.projroot, self.feature_sets, existing_data
|
|
)
|
|
|
|
def _update_meta_makefile(self) -> None:
|
|
self.enqueue_update('src/meta/Makefile')
|
|
|
|
def _generate_passthrough_file(self, path: str, existing_data: str) -> None:
|
|
self._generated_files[path] = existing_data
|
|
|
|
def _generate_meta_makefile(self, existing_data: str) -> None:
|
|
from batools.metamakefile import generate_meta_makefile
|
|
|
|
outfiles = generate_meta_makefile(self.projroot, existing_data)
|
|
for out_path, out_contents in outfiles.items():
|
|
self._generated_files[out_path] = out_contents
|
|
|
|
def _update_resources_makefile(self) -> None:
|
|
self.enqueue_update('src/resources/Makefile')
|
|
|
|
def _generate_resources_makefile(
|
|
self, path: str, existing_data: str
|
|
) -> None:
|
|
from batools.resourcesmakefile import ResourcesMakefileGenerator
|
|
|
|
self._generated_files[path] = ResourcesMakefileGenerator(
|
|
self.projroot,
|
|
existing_data,
|
|
projname=self.projname,
|
|
).run()
|