Skip to content

Commit 144d786

Browse files
authored
Firestore: cp_dlls.py added (#325)
1 parent 1d28a55 commit 144d786

File tree

1 file changed

+244
-0
lines changed

1 file changed

+244
-0
lines changed

cp_dlls.py

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
#!/usr/bin/env python
2+
"""
3+
%s <src_dir> <dest_dir> [dest_dir2, [dest_dir3, ...]] [flags]
4+
5+
Copies the macOS or Linux build artifacts of the firebase-unity-sdk to the
6+
directory of the Firestore Unity testapp.
7+
8+
This script is designed for use during iterative development. The idea is that
9+
you make changes to a source file, rebuild with cmake --build, then use this
10+
script to copy the build artifacts into a Unity project to test them out.
11+
12+
src_dir
13+
The directory into which the build artifacts of firebase-unity-sdk were
14+
written. This is the directory that was specified to cmake as the -B argument.
15+
16+
dest_dir
17+
The directories of the Unity projects into which to copy the build artifacts.
18+
The same "copy" steps will be repeated for each of these directories. Each of
19+
these Unity projects must have imported FirebaseFirestore.unitypackage at
20+
some point in the past, since this script only publishes a minimal subset of
21+
build artifacts and not everything needed for the Firebase Unity SDK.
22+
"""
23+
24+
# NOTE: This script requires Python 3.9 or newer.
25+
26+
from collections.abc import Iterator, Sequence
27+
import abc
28+
import enum
29+
import fnmatch
30+
import pathlib
31+
import re
32+
import shutil
33+
from typing import Optional
34+
35+
from absl import app
36+
from absl import flags
37+
from absl import logging
38+
39+
40+
FLAG_FILTER = flags.DEFINE_string(
41+
name="filter",
42+
default=None,
43+
help="The names of the files to include when copying; only the files whose "
44+
"names match this filter will be copied. The pattern uses Python's "
45+
"fnmatch module, which recognizes *, ?, and [anychar]. One use case would "
46+
"be to specify *dll so that only the .dll files are copied, since copying "
47+
"the .so files can crash the Unity Editor, and is not necessary if only C# "
48+
"files have been modified.",
49+
)
50+
51+
FLAG_DRY_RUN = flags.DEFINE_boolean(
52+
name="dry_run",
53+
default=False,
54+
help="Log what file operations would be performed, without actually "
55+
"performing them.",
56+
)
57+
58+
59+
def main(argv: Sequence[str]) -> None:
60+
if len(argv) < 2:
61+
raise app.UsageError("src_dir must be specified")
62+
elif len(argv) < 3:
63+
raise app.UsageError("at least one dest_dir must be specified")
64+
65+
src_dir = pathlib.Path(argv[1])
66+
dest_dirs = tuple(pathlib.Path(dest_dir_str) for dest_dir_str in argv[2:])
67+
68+
if FLAG_FILTER.value:
69+
copy_pattern = re.compile(fnmatch.translate(FLAG_FILTER.value))
70+
else:
71+
copy_pattern = None
72+
73+
if FLAG_DRY_RUN.value:
74+
fs = NoOpFileSystem()
75+
else:
76+
fs = RealFileSystem()
77+
78+
for dest_dir in dest_dirs:
79+
copier = BuildArtifactCopier(src_dir, dest_dir, fs, copy_pattern)
80+
copier.run()
81+
82+
83+
@enum.unique
84+
class OS(enum.Enum):
85+
MAC = (".bundle")
86+
LINUX = (".so")
87+
88+
def __init__(self, lib_filename_suffix: str) -> None:
89+
self.lib_filename_suffix = lib_filename_suffix
90+
91+
92+
class FileSystem(abc.ABC):
93+
94+
@abc.abstractmethod
95+
def unlink(self, path: pathlib.Path) -> None:
96+
raise NotImplementedError()
97+
98+
@abc.abstractmethod
99+
def copy_file(self, src: pathlib.Path, dest: pathlib.Path) -> None:
100+
raise NotImplementedError()
101+
102+
103+
class RealFileSystem(FileSystem):
104+
105+
def unlink(self, path: pathlib.Path) -> None:
106+
path.unlink()
107+
108+
def copy_file(self, src: pathlib.Path, dest: pathlib.Path) -> None:
109+
shutil.copy2(src, dest)
110+
111+
112+
class NoOpFileSystem(FileSystem):
113+
114+
def unlink(self, path: pathlib.Path) -> None:
115+
pass
116+
117+
def copy_file(self, src: pathlib.Path, dest: pathlib.Path) -> None:
118+
pass
119+
120+
121+
class BuildArtifactCopier:
122+
123+
def __init__(
124+
self,
125+
src_dir: pathlib.Path,
126+
dest_dir: pathlib.Path,
127+
fs: FileSystem,
128+
copy_pattern: Optional[re.Pattern],
129+
) -> None:
130+
self.src_dir = src_dir
131+
self.dest_dir = dest_dir
132+
self.fs = fs
133+
self.copy_pattern = copy_pattern
134+
135+
def run(self) -> None:
136+
logging.info("Copying build artifacts from %s to %s", self.src_dir, self.dest_dir)
137+
(src_cpp_app_library_os, src_cpp_app_library_file) = self.find_src_cpp_app_library()
138+
self.delete_old_cpp_app_library_files(src_cpp_app_library_os, src_cpp_app_library_file.name)
139+
self.copy_files(src_cpp_app_library_os, src_cpp_app_library_file)
140+
141+
def find_src_cpp_app_library(self) -> tuple[OS, pathlib.Path]:
142+
found_artifacts: list[tuple[OS, pathlib.Path]] = []
143+
144+
logging.info("Searching for C++ library build artifact files in directory: %s", self.src_dir)
145+
for candidate_artifact_path in self.src_dir.iterdir():
146+
for candidate_os in OS:
147+
if candidate_artifact_path.name.endswith(candidate_os.lib_filename_suffix):
148+
logging.info("Found C++ library build artifact file: %s", candidate_artifact_path)
149+
found_artifacts.append((candidate_os, candidate_artifact_path))
150+
151+
if len(found_artifacts) == 0:
152+
raise FirebaseCppLibraryBuildArtifactNotFound(
153+
f"No C++ library build artifact files found in {self.src_dir}, but "
154+
f"expected to find exactly one file whose name ends with one of the "
155+
f"following suffixes: " + ", ".join(os.lib_filename_suffix for os in OS)
156+
)
157+
elif len(found_artifacts) > 1:
158+
raise FirebaseCppLibraryBuildArtifactNotFound(
159+
f"Found {len(found_artifacts)} C++ library build artifacts in "
160+
f"{self.src_dir}, but expected to find exactly one: "
161+
+ ", ".join(found_artifact[1].name for found_artifact in found_artifacts)
162+
)
163+
164+
return found_artifacts[0]
165+
166+
def delete_old_cpp_app_library_files(
167+
self,
168+
os: OS,
169+
src_cpp_app_library_file_name: str
170+
) -> None:
171+
expr = re.compile(r"FirebaseCppApp-\d+_\d+_\d+" + re.escape(os.lib_filename_suffix))
172+
if not expr.fullmatch(src_cpp_app_library_file_name):
173+
raise ValueError(f"invalid filename: {src_cpp_app_library_file_name}")
174+
175+
# Do not delete old CPP app library files if we aren't even copying them
176+
# due to the copy_pattern not matching the file name.
177+
if self.copy_pattern is not None \
178+
and not self.copy_pattern.fullmatch(src_cpp_app_library_file_name):
179+
return
180+
181+
# TODO(dconeybe) Update this logic to also support Mac M1, which probably
182+
# uses a different directory than "x86_64".
183+
x86_64_dir = self.dest_dir / "Assets" / "Firebase" / "Plugins" / "x86_64"
184+
for candidate_dll_file in x86_64_dir.iterdir():
185+
if expr.fullmatch(candidate_dll_file.name):
186+
# Don't delete the file that is going to be overwritten anyways; because
187+
# deleting it is unnecessary and adds noise to the log output.
188+
if candidate_dll_file.name == src_cpp_app_library_file_name:
189+
continue
190+
logging.info("Deleting %s", candidate_dll_file)
191+
self.fs.unlink(candidate_dll_file)
192+
candidate_dll_meta_file = candidate_dll_file.parent \
193+
/ (candidate_dll_file.name + ".meta")
194+
if candidate_dll_meta_file.exists():
195+
logging.info("Deleting %s", candidate_dll_meta_file)
196+
self.fs.unlink(candidate_dll_meta_file)
197+
198+
def copy_files(self, os: OS, src_cpp_app_library_file: pathlib.Path) -> None:
199+
files_to_copy = sorted(self.files_to_copy(os, src_cpp_app_library_file))
200+
for (src_file, dest_file) in files_to_copy:
201+
if self.copy_pattern is None or self.copy_pattern.fullmatch(src_file.name):
202+
logging.info("Copying %s to %s", src_file, dest_file)
203+
self.fs.copy_file(src_file, dest_file)
204+
205+
def files_to_copy(
206+
self,
207+
os: OS,
208+
src_cpp_app_library_file: pathlib.Path
209+
) -> Iterator[tuple[pathlib.Path, pathlib.Path]]:
210+
# As a special case, yield the src_cpp_app_library_file.
211+
# Since its name encodes the version number, which changes over time, we
212+
# cannot simply hardcode it like the other files in the dict below.
213+
dest_cpp_app_library_file = self.dest_dir / "Assets" / "Firebase" \
214+
/ "Plugins" / "x86_64" / src_cpp_app_library_file.name
215+
yield (src_cpp_app_library_file, dest_cpp_app_library_file )
216+
217+
src_file_strs_by_dest_dir_str: dict[str, tuple[str, ...]] = {
218+
"Assets/Firebase/Plugins": (
219+
"app/obj/x86/Release/Firebase.App.dll",
220+
"app/platform/obj/x86/Release/Firebase.Platform.dll",
221+
"app/task_extension/obj/x86/Release/Firebase.TaskExtension.dll",
222+
"auth/obj/x86/Release/Firebase.Auth.dll",
223+
"firestore/obj/x86/Release/Firebase.Firestore.dll",
224+
),
225+
"Assets/Firebase/Plugins/x86_64": (
226+
"firestore/FirebaseCppFirestore" + os.lib_filename_suffix,
227+
"auth/FirebaseCppAuth" + os.lib_filename_suffix,
228+
),
229+
}
230+
231+
# Yield (src_file, dest_file) pairs from the dict above.
232+
for (dest_dir_str, src_file_strs) in src_file_strs_by_dest_dir_str.items():
233+
for src_file_str in src_file_strs:
234+
src_file = self.src_dir / src_file_str
235+
dest_file = self.dest_dir / dest_dir_str / src_file.name
236+
yield (src_file, dest_file)
237+
238+
239+
class FirebaseCppLibraryBuildArtifactNotFound(Exception):
240+
pass
241+
242+
243+
if __name__ == "__main__":
244+
app.run(main)

0 commit comments

Comments
 (0)