2022-08-16 16:47:17 +00:00
#!/usr/bin/env python3
# encoding: utf-8
2022-08-25 01:36:59 +00:00
# known issues:
# xcode - if a file already exists in 'wesnoth' target, then it incorrectly thinks it also exists in the 'tests' target even though the tests build will fail
2024-01-25 13:03:32 +00:00
"""
Add files to the specified build targets, supporting
CMake, SCons, Xcode and the Code::Blocks projects.
2022-08-16 16:47:17 +00:00
Valid build targets are:
2024-01-25 13:03:32 +00:00
* "wesnoth" - the main game (default if no target is specified)
2022-08-16 16:47:17 +00:00
* "wesnothd" - the wesnoth server
* "campaignd"
* "lua"
* "tests" - boost unit tests
2024-01-25 13:03:32 +00:00
The files will be added to:
2022-08-16 16:47:17 +00:00
* the lists used by CMake and SCons in "source_lists"
* the Xcode project
* The Code::Blocks project
This only supports files inside the "src" directory.
"""
2024-01-25 13:03:32 +00:00
import argparse
import sys
import inspect
import pathlib
try:
import pbxproj
except:
print('\n'.join((
'This script requires the "pbxproj" module.',
'Install it using "pip install pbxproj"',
'optionally setting up a python3-venv first.',
)))
exit(1)
2022-08-16 16:47:17 +00:00
#=========#
# Globals #
#=========#
# Either the executable directory or the current working directory
# should be the wesnoth root directory
rootdir = pathlib.Path(inspect.getsourcefile(lambda:0))
if not rootdir.joinpath("projectfiles").exists():
rootdir = pathlib.Path()
if not rootdir.joinpath("projectfiles").exists():
raise Exception("Could not find project file directory")
# the names of the targets in the Xcode project
xcode_target_translations = {
2023-03-18 16:03:49 +00:00
"wesnoth": ["The Battle for Wesnoth", "unit_tests"],
"wesnothd": ["wesnothd"],
"campaignd": ["campaignd"],
"lua": ["liblua"],
"tests": ["unit_tests"],
2022-08-16 16:47:17 +00:00
}
# the names of the targets in source_lists
source_list_target_translations = {
"wesnoth": "wesnoth",
"wesnothd": "wesnothd",
"campaignd": "campaignd",
"lua": "lua",
"tests": "boost_unit_tests",
}
# the names of the targets in Code::Blocks
code_blocks_target_translations = {
"wesnoth": "wesnoth",
"wesnothd": "wesnothd",
"campaignd": "campaignd",
"lua": "liblua",
"tests": "tests",
}
#=======#
# XCode #
#=======#
def add_to_xcode(filename, targets):
"""Add the given file to the specified targets.
"""
projectfile = rootdir.joinpath(
"projectfiles",
"Xcode",
"The Battle for Wesnoth.xcodeproj",
"project.pbxproj",
)
2024-01-26 12:16:13 +00:00
2022-08-16 16:47:17 +00:00
project = pbxproj.XcodeProject.load(projectfile)
2024-01-26 12:16:13 +00:00
2023-03-18 16:03:49 +00:00
translated_targets = [item for t in targets for item in xcode_target_translations[t]]
translated_targets = list(set(translated_targets))
2022-08-16 16:47:17 +00:00
print(" xcode targets:", translated_targets)
2024-01-26 12:16:13 +00:00
2022-08-16 16:47:17 +00:00
for tname in translated_targets:
if not project.get_target_by_name(tname):
raise Exception(
f"Could not find target '{tname}' in Xcode project file")
2024-01-26 12:16:13 +00:00
2022-08-16 16:47:17 +00:00
# groups are organized by directory structure under "src"
2024-08-05 16:42:20 +00:00
# except for tests, which have a separate root, "tests"
if pathlib.Path("tests") in filename.parents:
src_groups = project.get_groups_by_name("tests")
else:
src_groups = project.get_groups_by_name("src")
2022-08-16 16:47:17 +00:00
if len(src_groups) != 1:
raise Exception("problem finding 'src' group in xcode project")
src_group = src_groups[0]
parent_group = src_group
for d in filename.parts[:-1]:
2024-08-05 16:42:20 +00:00
if d == "tests":
continue
2022-08-16 16:47:17 +00:00
found_groups = project.get_groups_by_name(d, parent=parent_group)
if len(found_groups) != 1:
groupname = parent_group.get_name()
raise Exception(f"problem finding '{d}' group in '{groupname}'")
parent_group = found_groups[0]
2024-01-26 12:16:13 +00:00
2022-08-16 16:47:17 +00:00
# if the group already has an entry with the same filename, loudly skip.
# note: this doesn't allow adding to targets one at a time.
# a new file should be added to all targets at once...
# or maybe targets could be checked somehow,
# or maybe the file could simply be completely removed and readded.
if project.get_files_by_name(filename.name, parent=parent_group):
2022-08-25 01:36:59 +00:00
print(" '"+filename.name+"' already found in Xcode project '"+",".join(translated_targets)+"', skipping")
2022-08-16 16:47:17 +00:00
return
2024-01-26 12:16:13 +00:00
2022-08-16 16:47:17 +00:00
# force is True here because otherwise a duplicate filename in
# a different place will block addition of the new file.
# the rest is just to match existing project file structure.
2022-08-25 01:36:59 +00:00
project.add_file(filename.name,
2022-08-16 16:47:17 +00:00
force=True,
tree="<group>",
parent=parent_group,
target_name=translated_targets,
)
2024-01-26 12:16:13 +00:00
2022-08-16 16:47:17 +00:00
# that's done, save the file
project.save()
return
#==============#
# source_lists #
#==============#
def add_to_source_list(filename, source_list):
source_list_file = rootdir.joinpath("source_lists", source_list)
sl_lines = open(source_list_file).readlines()
file_line = filename.as_posix() + '\n'
2024-01-26 12:16:13 +00:00
2022-08-25 01:36:59 +00:00
# we only need source files in the source_lists, not header files
2023-03-18 16:03:49 +00:00
if filename.suffix != ".cpp":
2022-08-25 01:36:59 +00:00
return
2022-08-16 16:47:17 +00:00
# if the target already has an entry with the same filename, loudly skip
if file_line in sl_lines:
print(f" '{filename}' already found in '{source_list}', skipping")
return
2024-01-26 12:16:13 +00:00
2022-08-16 16:47:17 +00:00
sl_lines.append(file_line)
sl_lines.sort()
open(source_list_file, 'w').writelines(sl_lines)
2024-01-25 13:03:32 +00:00
def add_to_source_lists(filename, targets):
2022-08-16 16:47:17 +00:00
translated_targets = [source_list_target_translations[t] for t in targets]
print(" source_list targets:", translated_targets)
for t in translated_targets:
add_to_source_list(filename, t)
#==============#
# Code::Blocks #
#==============#
def add_to_code_blocks_target(filename, target):
cbp_file = rootdir.joinpath(
"projectfiles",
"CodeBlocks",
f"{target}.cbp",
)
cbp_lines = open(cbp_file).readlines()
2024-01-26 12:16:13 +00:00
2022-08-16 16:47:17 +00:00
filename_for_cbp = pathlib.PurePath(
"..", "..", "src", filename
).as_posix()
2024-01-26 12:16:13 +00:00
2022-08-16 16:47:17 +00:00
elem = f"\t\t<Unit filename=\"{filename_for_cbp}\" />\n"
2024-01-26 12:16:13 +00:00
2022-08-16 16:47:17 +00:00
# if the target already has an entry with the same filename, loudly skip
if elem in cbp_lines:
print(f" '{filename}' already found in '{target}.cbp', skipping")
return
2024-01-26 12:16:13 +00:00
2022-08-16 16:47:17 +00:00
# find an appropriate line to add before/after
index = 0
for line in cbp_lines:
if line.startswith("\t\t<Unit "):
if elem < line:
break
elif line.startswith("\t\t<Extensions>"):
# we must be the last entry, as this comes after the Unit section
break
index += 1
cbp_lines.insert(index, elem)
2024-01-26 12:16:13 +00:00
2022-08-16 16:47:17 +00:00
open(cbp_file, 'w').writelines(cbp_lines)
def add_to_code_blocks(filename, targets):
translated_targets = [code_blocks_target_translations[t] for t in targets]
print(" code::blocks targets:", translated_targets)
for t in translated_targets:
add_to_code_blocks_target(filename, t)
2024-01-26 12:16:13 +00:00
def sanity_check_existing_cpp_hpp(filenames):
"""
If we're adding a .cpp file, check whether a .hpp should be added too, etc.
Only the files named on the command line are added, this exits if the check fails.
"""
any_check_failed = False
for filename in filenames:
if filenames.count(filename) > 1:
print(f"ERROR: File '{filename}' given multiple times")
any_check_failed = True
if not rootdir.joinpath("src", filename).exists():
print(f"WARN: File '{filename}' does not exist")
any_check_failed = True
spouse = None
if filename.suffix == ".cpp":
spouse = filename.with_suffix(".hpp")
elif filename.suffix == ".hpp":
spouse = filename.with_suffix(".cpp")
if rootdir.joinpath("src", spouse).exists() and not filenames.count(spouse):
print(f"WARN: Requested to add '{filename}', should '{spouse}' be added too?")
any_check_failed = True
if any_check_failed:
break
if any_check_failed:
print("ERROR: Not making changes, as checks failed and --no-checks option was not used.")
exit(1)
def canonicalise_filenames(original_filenames):
"""
The script supports giving the filenames with or without the "src/" prefix.
Strip the "src/" if present, functions that need it will add it again later.
"""
filenames = []
# If src/src/ exists, the filenames become ambiguous. No need to support that.
if rootdir.joinpath("src", "src").exists():
print("Please don't add a file or directory called src/src.")
exit(1)
for filename in options.filename:
filename = pathlib.PurePath(filename)
parts = filename.parts
if parts[0] == "src":
filename = pathlib.PurePath(*parts[1:])
else:
filename = pathlib.PurePath(*parts)
filenames.append(filename)
return filenames
2022-08-16 16:47:17 +00:00
#======#
# main #
#======#
if __name__ == "__main__":
2024-01-25 13:03:32 +00:00
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
2022-08-16 16:47:17 +00:00
# a file argument is mandatory
2024-01-25 13:03:32 +00:00
ap.add_argument("filename", action="store", nargs="+",
help="the .cpp and .hpp files to add")
ap.add_argument("--target", action="store", nargs=1,
default=["wesnoth"],
help="which build targets to add the file to")
2024-01-26 12:16:13 +00:00
ap.add_argument("--no-checks", action="store_true",
help="do not check whether the files exist, etc")
2024-01-25 13:03:32 +00:00
# By default, recognise --help too
options = ap.parse_args()
# Bail out if someone uses the old syntax of "add_source_file src/foo.cpp campaignd"
2024-01-26 12:16:13 +00:00
if not options.no_checks:
if len(options.filename) == 2 and not options.filename[1].count('.'):
print("The usage has changed, targets now need to be given using --target name")
exit(1)
2024-01-25 13:03:32 +00:00
2024-01-26 12:16:13 +00:00
# Convert the names to pathlib.PurePath objects without leading "src/"
filenames = canonicalise_filenames(options.filename)
2024-01-25 13:03:32 +00:00
2024-01-26 12:16:13 +00:00
if not options.no_checks:
sanity_check_existing_cpp_hpp(filenames)
2024-01-25 13:03:32 +00:00
2024-01-26 12:16:13 +00:00
for filename in filenames:
2024-01-25 13:03:32 +00:00
print(f"adding '{filename}' to targets: {options.target}")
add_to_xcode(filename, options.target)
add_to_source_lists(filename, options.target)
add_to_code_blocks(filename, options.target)