Skip to content

Commit 8fa3d93

Browse files
author
Stewart Miles
committed
Added a script to import a package into a Unity project.
If a Unity project fails to compile it's not possible to import packages with the -importPackage command line flag for automation. import_unity_package.py manually unpacks a .unitypackage archive into a project without loading Unity. Change-Id: Iffbd13750ef02a3d8129cc17a250f51adec654ce
1 parent 51b1e10 commit 8fa3d93

8 files changed

+374
-0
lines changed

build.gradle

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,8 @@ project.ext {
269269

270270
// Location of the export_unity_package application.
271271
exportUnityPackageDir = new File(pluginSourceDir, "ExportUnityPackage")
272+
// Location of the import_unity_package application.
273+
importUnityPackageDir = new File(pluginSourceDir, "ImportUnityPackage")
272274

273275
// Common arguments used to execute Unity in batch mode.
274276
unityBatchModeArguments = ["-batchmode", "-nographics"]
@@ -1359,6 +1361,14 @@ createPythonTask(
13591361
[],
13601362
["absl-py"])
13611363

1364+
createPythonTask(
1365+
"testImportUnityPackage",
1366+
"Test the import_unity_package.py application",
1367+
[],
1368+
new File(project.ext.importUnityPackageDir, "import_unity_package_test.py"),
1369+
[],
1370+
["absl-py"])
1371+
13621372
task updateEmbeddedGradleWrapper(type: Zip) {
13631373
description "Update the gradle wrapper in gradle-template.zip"
13641374
from project.ext.scriptDirectory
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
#!/usr/bin/python
2+
#
3+
# Copyright 2020 Google LLC
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
r"""A script to import a .unitypackage into a project without Unity.
18+
19+
Example usage:
20+
import_unity_package.py --projects=path/to/unity/project \
21+
--packages=mypackage.unitypackage
22+
"""
23+
24+
import os
25+
import shutil
26+
import tarfile
27+
import tempfile
28+
from absl import app
29+
from absl import flags
30+
from absl import logging
31+
32+
FLAGS = flags.FLAGS
33+
34+
flags.DEFINE_multi_string(
35+
"projects", None, "Paths to Unity project directories to unpack packages "
36+
"into. This should be the directory that contains the Assets directory, "
37+
"i.e my/project not my/project/Assets")
38+
flags.DEFINE_multi_string(
39+
"packages", None, "Set of packages to unpack into a project. Packages are "
40+
"unpacked in the order they're specified.")
41+
42+
43+
def files_exist(paths_to_check):
44+
"""Determine whether the specified files exist.
45+
46+
Args:
47+
paths_to_check: List of files to check whether they exist.
48+
49+
Returns:
50+
List of files that do not exist.
51+
"""
52+
return [p for p in paths_to_check if not os.path.isfile(os.path.realpath(p))]
53+
54+
55+
def directories_exist(paths_to_check):
56+
"""Determine whether the specified directories exist.
57+
58+
Args:
59+
paths_to_check: List of directories to check whether they exist.
60+
61+
Returns:
62+
List of directories that do not exist.
63+
"""
64+
return [p for p in paths_to_check if not os.path.isdir(os.path.realpath(p))]
65+
66+
67+
def unpack_to_directory(directory, packages):
68+
"""Unpack a set of .unitypackage files to a directory.
69+
70+
Args:
71+
directory: Directory to unpack into.
72+
packages: List of .unitypackage filesname to unpack.
73+
74+
Returns:
75+
Dictionary containing a list of files that could not be extracted, keyed by
76+
package archive filename.
77+
"""
78+
ignored_files_by_package = {}
79+
for unitypackage in packages:
80+
with tarfile.open(unitypackage) as unitypackage_file:
81+
member_names = unitypackage_file.getnames()
82+
guid_to_path = {}
83+
extracted_files = set()
84+
85+
# Map each asset GUID to an extract path the path of each extracted asset.
86+
for filename in member_names:
87+
if os.path.basename(filename) == "pathname":
88+
guid = os.path.dirname(filename)
89+
with unitypackage_file.extractfile(filename) as pathname_file:
90+
pathname = pathname_file.read().decode("utf8").strip()
91+
if guid and pathname:
92+
extracted_files.add(filename)
93+
guid_to_path[guid] = pathname
94+
95+
# Extract each asset to the appropriate path in the output directory.
96+
for filename in member_names:
97+
basename = os.path.basename(filename)
98+
if basename == "asset" or basename == "asset.meta":
99+
guid = os.path.dirname(filename)
100+
if guid:
101+
pathname = guid_to_path.get(guid)
102+
if pathname:
103+
with unitypackage_file.extractfile(filename) as member_file:
104+
extension = os.path.splitext(basename)[1]
105+
output_filename = os.path.join(directory, pathname + extension)
106+
os.makedirs(os.path.dirname(output_filename), exist_ok=True)
107+
with open(output_filename, "wb") as output_file:
108+
shutil.copyfileobj(member_file, output_file)
109+
extracted_files.add(filename)
110+
111+
# Returns the list of files that could not be extracted in the archive's
112+
# order.
113+
ignored_files = []
114+
for member in member_names:
115+
if member not in extracted_files:
116+
if unitypackage_file.getmember(member).isfile():
117+
ignored_files.append(member)
118+
if ignored_files:
119+
ignored_files_by_package[unitypackage] = ignored_files
120+
return ignored_files_by_package
121+
122+
123+
def main(unused_argv):
124+
"""Unpacks a set of .unitypackage files into a set of Unity projects.
125+
126+
Args:
127+
unused_argv: Not used.
128+
129+
Returns:
130+
0 if successful, 1 otherwise.
131+
"""
132+
# Make sure all input files and output directories exist.
133+
missing_packages = files_exist(FLAGS.packages)
134+
missing_projects = directories_exist(FLAGS.projects)
135+
if missing_packages:
136+
logging.error("Specified packages %s not found.", missing_packages)
137+
if missing_projects:
138+
logging.error("Specified projects %s not found.", missing_projects)
139+
if missing_packages or missing_projects:
140+
return 1
141+
142+
with tempfile.TemporaryDirectory() as unpack_directory:
143+
# Unpack all packages into a single directory.
144+
for package, files in unpack_to_directory(unpack_directory, FLAGS.packages):
145+
logging.error("Failed to unpack files %s from package %s", package, files)
146+
147+
# Copy unpacked packages into each project.
148+
for project in FLAGS.projects:
149+
for dirname, _, filenames in os.walk(unpack_directory):
150+
for filename in filenames:
151+
source_filename = os.path.join(dirname, filename)
152+
relative_filename = source_filename[len(unpack_directory) + 1:]
153+
if os.path.isfile(source_filename):
154+
target_filename = os.path.join(project, relative_filename)
155+
os.makedirs(os.path.dirname(target_filename), exist_ok=True)
156+
shutil.copyfile(source_filename, target_filename)
157+
return 0
158+
159+
160+
if __name__ == "__main__":
161+
flags.mark_flag_as_required("projects")
162+
flags.mark_flag_as_required("packages")
163+
app.run(main)
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
#!/usr/bin/python
2+
#
3+
# Copyright 2020 Google LLC
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
"""Tests for import_unity_package.py."""
18+
19+
import os
20+
import shutil
21+
import sys
22+
from absl import flags
23+
from absl.testing import absltest
24+
25+
# pylint: disable=C6204
26+
# pylint: disable=W0403
27+
sys.path.append(os.path.dirname(__file__))
28+
import import_unity_package
29+
# pylint: enable=C6204
30+
# pylint: enable=W0403
31+
32+
FLAGS = flags.FLAGS
33+
34+
# Location of test data.
35+
TEST_DATA_PATH = os.path.join(os.path.dirname(__file__), "test_data")
36+
37+
class ImportUnityPackageTest(absltest.TestCase):
38+
"""Test import_unity_package.py."""
39+
40+
def setUp(self):
41+
"""Create a temporary directory."""
42+
self.temp_dir = os.path.join(FLAGS.test_tmpdir, "temp")
43+
os.makedirs(self.temp_dir)
44+
45+
def tearDown(self):
46+
"""Clean up the temporary directory."""
47+
shutil.rmtree(self.temp_dir)
48+
49+
def test_files_exist(self):
50+
"""Test file existance check."""
51+
non_existent_file = os.path.join(FLAGS.test_tmpdir, "foo/bar.txt")
52+
existent_file = os.path.join(FLAGS.test_tmpdir, "a_file.txt")
53+
with open(existent_file, "wt") as test_file:
54+
test_file.write("hello")
55+
self.assertEqual(
56+
import_unity_package.files_exist([existent_file, non_existent_file]),
57+
[non_existent_file])
58+
59+
def test_directories_exist(self):
60+
"""Test directory existence check."""
61+
non_existent_dir = os.path.join(FLAGS.test_tmpdir, "foo/bar")
62+
existent_dir = os.path.join(FLAGS.test_tmpdir, "an/available/dir")
63+
os.makedirs(existent_dir, exist_ok=True)
64+
self.assertEqual(
65+
import_unity_package.directories_exist([non_existent_dir,
66+
existent_dir]),
67+
[non_existent_dir])
68+
69+
def read_contents_file(self, test_package_filename):
70+
"""Read the contents file for the specified test package.
71+
72+
Args:
73+
test_package_filename: File to read the expected contents of.
74+
75+
Returns:
76+
Sorted list of filenames read from
77+
(test_package_filename + ".contents.txt").
78+
"""
79+
contents = []
80+
with open(test_package_filename + ".contents.txt", "rt") as contents_file:
81+
return [l.strip() for l in contents_file.readlines() if l.strip()]
82+
83+
def list_files_in_temp_dir(self):
84+
"""List files in the temporary directory.
85+
86+
Returns:
87+
Sorted list of files relative to the temporary directory.
88+
"""
89+
files = []
90+
for dirpath, _, filenames in os.walk(self.temp_dir):
91+
for basename in list(filenames):
92+
filename = os.path.join(dirpath, basename)
93+
if os.path.isfile(filename):
94+
files.append(filename[len(self.temp_dir) + 1:])
95+
return sorted(files)
96+
97+
def test_unpack_to_directory_valid_archive(self):
98+
"""Unpack a valid unitypackage into a directory."""
99+
packages = [
100+
os.path.join(TEST_DATA_PATH,
101+
"external-dependency-manager-1.2.144.unitypackage"),
102+
os.path.join(TEST_DATA_PATH,
103+
"external-dependency-manager-1.2.153.unitypackage")
104+
]
105+
self.assertEqual(import_unity_package.unpack_to_directory(self.temp_dir,
106+
packages), {})
107+
108+
expected_files = set(self.read_contents_file(packages[0]))
109+
expected_files = expected_files.union(self.read_contents_file(packages[1]))
110+
111+
self.maxDiff = None
112+
self.assertEqual(self.list_files_in_temp_dir(),
113+
sorted(expected_files))
114+
115+
def test_unpack_to_directory_invalid_archive(self):
116+
"""Unpack a broken unitypackage into a directory."""
117+
# This archive has been modified so that 9b7b6f84d4eb4f549252df73305e17c8
118+
# does not have a path.
119+
packages = [
120+
os.path.join(
121+
TEST_DATA_PATH,
122+
"external-dependency-manager-1.2.144-broken.unitypackage")
123+
]
124+
self.assertEqual(
125+
import_unity_package.unpack_to_directory(self.temp_dir, packages),
126+
{packages[0]: [
127+
"9b7b6f84d4eb4f549252df73305e17c8/asset.meta",
128+
"9b7b6f84d4eb4f549252df73305e17c8/asset"]})
129+
130+
131+
if __name__ == "__main__":
132+
absltest.main()
133+
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
Assets/ExternalDependencyManager.meta
2+
Assets/ExternalDependencyManager/Editor.meta
3+
Assets/ExternalDependencyManager/Editor/CHANGELOG.md
4+
Assets/ExternalDependencyManager/Editor/CHANGELOG.md.meta
5+
Assets/ExternalDependencyManager/Editor/Google.IOSResolver_v1.2.144.dll
6+
Assets/ExternalDependencyManager/Editor/Google.IOSResolver_v1.2.144.dll.mdb
7+
Assets/ExternalDependencyManager/Editor/Google.IOSResolver_v1.2.144.dll.mdb.meta
8+
Assets/ExternalDependencyManager/Editor/Google.IOSResolver_v1.2.144.dll.meta
9+
Assets/ExternalDependencyManager/Editor/Google.JarResolver_v1.2.144.dll
10+
Assets/ExternalDependencyManager/Editor/Google.JarResolver_v1.2.144.dll.mdb
11+
Assets/ExternalDependencyManager/Editor/Google.JarResolver_v1.2.144.dll.mdb.meta
12+
Assets/ExternalDependencyManager/Editor/Google.JarResolver_v1.2.144.dll.meta
13+
Assets/ExternalDependencyManager/Editor/Google.UnityPackageManagerResolver_v1.2.144.dll
14+
Assets/ExternalDependencyManager/Editor/Google.UnityPackageManagerResolver_v1.2.144.dll.mdb
15+
Assets/ExternalDependencyManager/Editor/Google.UnityPackageManagerResolver_v1.2.144.dll.mdb.meta
16+
Assets/ExternalDependencyManager/Editor/Google.UnityPackageManagerResolver_v1.2.144.dll.meta
17+
Assets/ExternalDependencyManager/Editor/Google.VersionHandler.dll
18+
Assets/ExternalDependencyManager/Editor/Google.VersionHandler.dll.mdb
19+
Assets/ExternalDependencyManager/Editor/Google.VersionHandler.dll.mdb.meta
20+
Assets/ExternalDependencyManager/Editor/Google.VersionHandler.dll.meta
21+
Assets/ExternalDependencyManager/Editor/Google.VersionHandlerImpl_v1.2.144.dll
22+
Assets/ExternalDependencyManager/Editor/Google.VersionHandlerImpl_v1.2.144.dll.mdb
23+
Assets/ExternalDependencyManager/Editor/Google.VersionHandlerImpl_v1.2.144.dll.mdb.meta
24+
Assets/ExternalDependencyManager/Editor/Google.VersionHandlerImpl_v1.2.144.dll.meta
25+
Assets/ExternalDependencyManager/Editor/GoogleRegistries.xml
26+
Assets/ExternalDependencyManager/Editor/GoogleRegistries.xml.meta
27+
Assets/ExternalDependencyManager/Editor/LICENSE
28+
Assets/ExternalDependencyManager/Editor/LICENSE.meta
29+
Assets/ExternalDependencyManager/Editor/README.md
30+
Assets/ExternalDependencyManager/Editor/README.md.meta
31+
Assets/ExternalDependencyManager/Editor/external-dependency-manager_v1.2.144.txt
32+
Assets/ExternalDependencyManager/Editor/external-dependency-manager_v1.2.144.txt.meta
33+
Assets/PlayServicesResolver.meta
34+
Assets/PlayServicesResolver/Editor.meta
35+
Assets/PlayServicesResolver/Editor/play-services-resolver_v1.2.137.0.txt
36+
Assets/PlayServicesResolver/Editor/play-services-resolver_v1.2.137.0.txt.meta
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
Assets/ExternalDependencyManager/Editor/CHANGELOG.md
2+
Assets/ExternalDependencyManager/Editor/CHANGELOG.md.meta
3+
Assets/ExternalDependencyManager/Editor/Google.IOSResolver_v1.2.153.dll
4+
Assets/ExternalDependencyManager/Editor/Google.IOSResolver_v1.2.153.dll.mdb
5+
Assets/ExternalDependencyManager/Editor/Google.IOSResolver_v1.2.153.dll.mdb.meta
6+
Assets/ExternalDependencyManager/Editor/Google.IOSResolver_v1.2.153.dll.meta
7+
Assets/ExternalDependencyManager/Editor/Google.JarResolver_v1.2.153.dll
8+
Assets/ExternalDependencyManager/Editor/Google.JarResolver_v1.2.153.dll.mdb
9+
Assets/ExternalDependencyManager/Editor/Google.JarResolver_v1.2.153.dll.mdb.meta
10+
Assets/ExternalDependencyManager/Editor/Google.JarResolver_v1.2.153.dll.meta
11+
Assets/ExternalDependencyManager/Editor/Google.PackageManagerResolver_v1.2.153.dll
12+
Assets/ExternalDependencyManager/Editor/Google.PackageManagerResolver_v1.2.153.dll.mdb
13+
Assets/ExternalDependencyManager/Editor/Google.PackageManagerResolver_v1.2.153.dll.mdb.meta
14+
Assets/ExternalDependencyManager/Editor/Google.PackageManagerResolver_v1.2.153.dll.meta
15+
Assets/ExternalDependencyManager/Editor/Google.VersionHandler.dll
16+
Assets/ExternalDependencyManager/Editor/Google.VersionHandler.dll.mdb
17+
Assets/ExternalDependencyManager/Editor/Google.VersionHandler.dll.mdb.meta
18+
Assets/ExternalDependencyManager/Editor/Google.VersionHandler.dll.meta
19+
Assets/ExternalDependencyManager/Editor/Google.VersionHandlerImpl_v1.2.153.dll
20+
Assets/ExternalDependencyManager/Editor/Google.VersionHandlerImpl_v1.2.153.dll.mdb
21+
Assets/ExternalDependencyManager/Editor/Google.VersionHandlerImpl_v1.2.153.dll.mdb.meta
22+
Assets/ExternalDependencyManager/Editor/Google.VersionHandlerImpl_v1.2.153.dll.meta
23+
Assets/ExternalDependencyManager/Editor/GoogleRegistries.xml
24+
Assets/ExternalDependencyManager/Editor/GoogleRegistries.xml.meta
25+
Assets/ExternalDependencyManager/Editor/LICENSE
26+
Assets/ExternalDependencyManager/Editor/LICENSE.meta
27+
Assets/ExternalDependencyManager/Editor/README.md
28+
Assets/ExternalDependencyManager/Editor/README.md.meta
29+
Assets/ExternalDependencyManager/Editor/external-dependency-manager_version-1.2.153_manifest.txt
30+
Assets/ExternalDependencyManager/Editor/external-dependency-manager_version-1.2.153_manifest.txt.meta
31+
Assets/PlayServicesResolver/Editor/play-services-resolver_v1.2.137.0.txt
32+
Assets/PlayServicesResolver/Editor/play-services-resolver_v1.2.137.0.txt.meta

0 commit comments

Comments
 (0)