diff --git a/python/mozbuild/mozbuild/backend/android_eclipse.py b/python/mozbuild/mozbuild/backend/android_eclipse.py new file mode 100644 index 000000000000..31da09a71096 --- /dev/null +++ b/python/mozbuild/mozbuild/backend/android_eclipse.py @@ -0,0 +1,190 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from __future__ import unicode_literals + +import itertools +import os +import xml.etree.ElementTree as ET + +from mozpack.copier import FileCopier +from mozpack.files import (FileFinder, PreprocessedFile) +from mozpack.manifests import InstallManifest +import mozpack.path as mozpath + +from .common import CommonBackend +from ..frontend.data import ( + AndroidEclipseProjectData, + SandboxDerived, + SandboxWrapped, +) +from ..makeutil import Makefile +from ..util import ensureParentDir + + +class AndroidEclipseBackend(CommonBackend): + """Backend that generates Android Eclipse project files. + """ + + def _init(self): + CommonBackend._init(self) + + + def consume_object(self, obj): + """Write out Android Eclipse project files.""" + + if not isinstance(obj, SandboxDerived): + return + + CommonBackend.consume_object(self, obj) + + # If CommonBackend acknowledged the object, we're done with it. + if obj._ack: + return + + # We don't want to handle most things, so we just acknowledge all objects... + obj.ack() + + # ... and handle the one case we care about specially. + if isinstance(obj, SandboxWrapped) and isinstance(obj.wrapped, AndroidEclipseProjectData): + self._process_android_eclipse_project_data(obj.wrapped, obj.srcdir, obj.objdir) + + def consume_finished(self): + """The common backend handles WebIDL and test files. We don't handle + these, so we don't call our superclass. + """ + + def _Element_for_classpathentry(self, cpe): + """Turn a ClassPathEntry into an XML Element, like one of: + + + + + + + + """ + e = ET.Element('classpathentry') + e.set('kind', 'src') + e.set('including', '**/*.java') + e.set('path', cpe.path) + if cpe.exclude_patterns: + e.set('excluding', '|'.join(sorted(cpe.exclude_patterns))) + if cpe.ignore_warnings: + attrs = ET.SubElement(e, 'attributes') + attr = ET.SubElement(attrs, 'attribute') + attr.set('name', 'ignore_optional_problems') + attr.set('value', 'true') + return e + + def _Element_for_referenced_project(self, name): + """Turn a referenced project name into an XML Element, like: + + """ + e = ET.Element('classpathentry') + e.set('kind', 'src') + e.set('combineaccessrules', 'false') + # All project directories are in the same root; this + # reference is absolute in the Eclipse namespace. + e.set('path', '/' + name) + return e + + def _Element_for_extra_jar(self, name): + """Turn a referenced JAR name into an XML Element, like: + + """ + e = ET.Element('classpathentry') + e.set('kind', 'lib') + # All project directories are in the same root; this + # reference is absolute in the Eclipse namespace. + e.set('path', 'libs/' + name) + return e + + def _manifest_for_project(self, srcdir, project): + manifest = InstallManifest() + + if project.manifest: + manifest.add_copy(mozpath.join(srcdir, project.manifest), 'AndroidManifest.xml') + + if project.res: + manifest.add_symlink(mozpath.join(srcdir, project.res), 'res') + + if project.assets: + manifest.add_symlink(mozpath.join(srcdir, project.assets), 'assets') + + for cpe in project._classpathentries: + manifest.add_symlink(mozpath.join(srcdir, cpe.srcdir), cpe.dstdir) + + # JARs and native libraries go in the same place. This + # wouldn't be a problem, except we only know the contents of + # (a subdirectory of) libs/ after a successful build and + # package, which is after build-backend time. So we use a + # pattern symlink that is resolved at manifest install time. + if project.libs: + manifest.add_pattern_copy(mozpath.join(srcdir, project.libs), '**', 'libs') + + for extra_jar in sorted(project.extra_jars): + manifest.add_copy(mozpath.join(srcdir, extra_jar), mozpath.join('libs', os.path.basename(extra_jar))) + + return manifest + + def _process_android_eclipse_project_data(self, data, srcdir, objdir): + # This can't be relative to the environment's topsrcdir, + # because during testing topsrcdir is faked. + template_directory = os.path.abspath(mozpath.join(os.path.dirname(__file__), + 'templates', 'android_eclipse')) + + project_directory = mozpath.join(self.environment.topobjdir, 'android_eclipse', data.name) + manifest_path = mozpath.join(self.environment.topobjdir, 'android_eclipse', '%s.manifest' % data.name) + + manifest = self._manifest_for_project(srcdir, data) + ensureParentDir(manifest_path) + manifest.write(path=manifest_path) + + classpathentries = [] + for cpe in sorted(data._classpathentries, key=lambda x: x.path): + e = self._Element_for_classpathentry(cpe) + classpathentries.append(ET.tostring(e)) + + for name in sorted(data.referenced_projects): + e = self._Element_for_referenced_project(name) + classpathentries.append(ET.tostring(e)) + + for name in sorted(data.extra_jars): + e = self._Element_for_extra_jar(os.path.basename(name)) + classpathentries.append(ET.tostring(e)) + + defines = {} + defines['IDE_OBJDIR'] = objdir + defines['IDE_TOPOBJDIR'] = self.environment.topobjdir + defines['IDE_SRCDIR'] = srcdir + defines['IDE_TOPSRCDIR'] = self.environment.topsrcdir + defines['IDE_PROJECT_NAME'] = data.name + defines['IDE_PACKAGE_NAME'] = data.package_name + defines['IDE_PROJECT_DIRECTORY'] = project_directory + defines['IDE_RELSRCDIR'] = mozpath.relpath(srcdir, self.environment.topsrcdir) + defines['IDE_CLASSPATH_ENTRIES'] = '\n'.join('\t' + cpe for cpe in classpathentries) + defines['IDE_RECURSIVE_MAKE_TARGETS'] = ' '.join(sorted(data.recursive_make_targets)) + # Like android.library=true + defines['IDE_PROJECT_LIBRARY_SETTING'] = 'android.library=true' if data.is_library else '' + # Like android.library.reference.1=FennecBrandingResources + defines['IDE_PROJECT_LIBRARY_REFERENCES'] = '\n'.join( + 'android.library.reference.%s=%s' % (i + 1, ref) + for i, ref in enumerate(sorted(data.included_projects))) + + copier = FileCopier() + finder = FileFinder(template_directory) + for input_filename, f in itertools.chain(finder.find('**'), finder.find('.**')): + if input_filename == 'AndroidManifest.xml' and not data.is_library: + # Main projects supply their own manifests. + continue + copier.add(input_filename, PreprocessedFile( + mozpath.join(finder.base, input_filename), + depfile_path=None, + marker='#', + defines=defines, + extra_depends={mozpath.join(finder.base, input_filename)})) + + # When we re-create the build backend, we kill everything that was there. + copier.copy(project_directory, skip_if_older=False, remove_unaccounted=True) diff --git a/python/mozbuild/mozbuild/test/backend/data/android_eclipse/library1/resources/values/strings.xml b/python/mozbuild/mozbuild/test/backend/data/android_eclipse/library1/resources/values/strings.xml new file mode 100644 index 000000000000..a7337c554091 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/android_eclipse/library1/resources/values/strings.xml @@ -0,0 +1 @@ +library1 diff --git a/python/mozbuild/mozbuild/test/backend/data/android_eclipse/main1/AndroidManifest.xml b/python/mozbuild/mozbuild/test/backend/data/android_eclipse/main1/AndroidManifest.xml new file mode 100644 index 000000000000..7a906454df83 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/android_eclipse/main1/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/python/mozbuild/mozbuild/test/backend/data/android_eclipse/main2/AndroidManifest.xml b/python/mozbuild/mozbuild/test/backend/data/android_eclipse/main2/AndroidManifest.xml new file mode 100644 index 000000000000..7a906454df83 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/android_eclipse/main2/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/python/mozbuild/mozbuild/test/backend/data/android_eclipse/main2/assets/dummy.txt b/python/mozbuild/mozbuild/test/backend/data/android_eclipse/main2/assets/dummy.txt new file mode 100644 index 000000000000..c32a9599326a --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/android_eclipse/main2/assets/dummy.txt @@ -0,0 +1 @@ +# Placeholder. \ No newline at end of file diff --git a/python/mozbuild/mozbuild/test/backend/data/android_eclipse/main2/extra.jar b/python/mozbuild/mozbuild/test/backend/data/android_eclipse/main2/extra.jar new file mode 100644 index 000000000000..c32a9599326a --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/android_eclipse/main2/extra.jar @@ -0,0 +1 @@ +# Placeholder. \ No newline at end of file diff --git a/python/mozbuild/mozbuild/test/backend/data/android_eclipse/main2/res/values/strings.xml b/python/mozbuild/mozbuild/test/backend/data/android_eclipse/main2/res/values/strings.xml new file mode 100644 index 000000000000..0b28bf41ecf6 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/android_eclipse/main2/res/values/strings.xml @@ -0,0 +1 @@ +main1 diff --git a/python/mozbuild/mozbuild/test/backend/data/android_eclipse/main3/AndroidManifest.xml b/python/mozbuild/mozbuild/test/backend/data/android_eclipse/main3/AndroidManifest.xml new file mode 100644 index 000000000000..7a906454df83 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/android_eclipse/main3/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/python/mozbuild/mozbuild/test/backend/data/android_eclipse/main3/a/A.java b/python/mozbuild/mozbuild/test/backend/data/android_eclipse/main3/a/A.java new file mode 100644 index 000000000000..0ab867d3d75a --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/android_eclipse/main3/a/A.java @@ -0,0 +1 @@ +package a.a; diff --git a/python/mozbuild/mozbuild/test/backend/data/android_eclipse/main3/b/B.java b/python/mozbuild/mozbuild/test/backend/data/android_eclipse/main3/b/B.java new file mode 100644 index 000000000000..66eb44c15027 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/android_eclipse/main3/b/B.java @@ -0,0 +1 @@ +package b; diff --git a/python/mozbuild/mozbuild/test/backend/data/android_eclipse/main3/c/C.java b/python/mozbuild/mozbuild/test/backend/data/android_eclipse/main3/c/C.java new file mode 100644 index 000000000000..ca474ff33a4e --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/android_eclipse/main3/c/C.java @@ -0,0 +1 @@ +package d.e; diff --git a/python/mozbuild/mozbuild/test/backend/data/android_eclipse/main4 b/python/mozbuild/mozbuild/test/backend/data/android_eclipse/main4 new file mode 100644 index 000000000000..7a906454df83 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/android_eclipse/main4 @@ -0,0 +1 @@ + diff --git a/python/mozbuild/mozbuild/test/backend/data/android_eclipse/moz.build b/python/mozbuild/mozbuild/test/backend/data/android_eclipse/moz.build new file mode 100644 index 000000000000..955148bc29ac --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/android_eclipse/moz.build @@ -0,0 +1,37 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +p = add_android_eclipse_library_project('library1') +p.package_name = 'org.mozilla.test.library1' +p.res = 'library1/resources' + +p = add_android_eclipse_library_project('library2') +p.package_name = 'org.mozilla.test.library2' + +p = add_android_eclipse_project('main1', 'main1/AndroidManifest.xml') +p.package_name = 'org.mozilla.test.main1' +p.recursive_make_targets += ['target1', 'target2'] + +p = add_android_eclipse_project('main2', 'main2/AndroidManifest.xml') +p.package_name = 'org.mozilla.test.main2' +p.res = 'main2/res' +p.assets = 'main2/assets' +p.extra_jars = ['main2/extra.jar'] + +p = add_android_eclipse_project('main3', 'main3/AndroidManifest.xml') +p.package_name = 'org.mozilla.test.main3' +cpe = p.add_classpathentry('a', 'main3/a', dstdir='a/a') +cpe = p.add_classpathentry('b', 'main3/b', dstdir='b') +cpe.exclude_patterns += ['b/Excludes.java', 'b/Excludes2.java'] +cpe = p.add_classpathentry('c', 'main3/c', dstdir='d/e') +cpe.ignore_warnings = True + +p = add_android_eclipse_project('main4', 'main3/AndroidManifest.xml') +p.package_name = 'org.mozilla.test.main3' +p.referenced_projects += ['library1'] +p.included_projects += ['library2'] +p.recursive_make_targets += ['target3', 'target4'] + +DIRS += ['subdir'] diff --git a/python/mozbuild/mozbuild/test/backend/data/android_eclipse/subdir/moz.build b/python/mozbuild/mozbuild/test/backend/data/android_eclipse/subdir/moz.build new file mode 100644 index 000000000000..064a339d01b1 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/android_eclipse/subdir/moz.build @@ -0,0 +1,13 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +DEFINES['FOO'] = 'FOO' + +p = add_android_eclipse_library_project('sublibrary') +p.package_name = 'org.mozilla.test.sublibrary' +p.is_library = True + +p = add_android_eclipse_project('submain', 'submain/AndroidManifest.xml') +p.package_name = 'org.mozilla.test.submain' +p.recursive_make_targets += ['subtarget1', 'subtarget2'] diff --git a/python/mozbuild/mozbuild/test/backend/data/android_eclipse/subdir/submain/AndroidManifest.xml b/python/mozbuild/mozbuild/test/backend/data/android_eclipse/subdir/submain/AndroidManifest.xml new file mode 100644 index 000000000000..7a906454df83 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/data/android_eclipse/subdir/submain/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/python/mozbuild/mozbuild/test/backend/test_android_eclipse.py b/python/mozbuild/mozbuild/test/backend/test_android_eclipse.py new file mode 100644 index 000000000000..9d345d2a7fd7 --- /dev/null +++ b/python/mozbuild/mozbuild/test/backend/test_android_eclipse.py @@ -0,0 +1,151 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from __future__ import unicode_literals + +import json +import os +import unittest + +from mozbuild.backend.android_eclipse import AndroidEclipseBackend +from mozbuild.frontend.emitter import TreeMetadataEmitter +from mozbuild.frontend.reader import BuildReader +from mozbuild.test.backend.common import BackendTester +from mozpack.manifests import InstallManifest +from mozunit import main + +import mozpack.path as mozpath + +class TestAndroidEclipseBackend(BackendTester): + def __init__(self, *args, **kwargs): + BackendTester.__init__(self, *args, **kwargs) + self.env = None + + def assertExists(self, *args): + p = mozpath.join(self.env.topobjdir, 'android_eclipse', *args) + self.assertTrue(os.path.exists(p), "Path %s exists" % p) + + def assertNotExists(self, *args): + p = mozpath.join(self.env.topobjdir, 'android_eclipse', *args) + self.assertFalse(os.path.exists(p), "Path %s does not exist" % p) + + def test_library_project_files(self): + """Ensure we generate reasonable files for library projects.""" + self.env = self._consume('android_eclipse', AndroidEclipseBackend) + for f in ['.classpath', + '.project', + '.settings', + 'AndroidManifest.xml', + 'project.properties']: + self.assertExists('library1', f) + + def test_main_project_files(self): + """Ensure we generate reasonable files for main (non-library) projects.""" + self.env = self._consume('android_eclipse', AndroidEclipseBackend) + for f in ['.classpath', + '.externalToolBuilders', + '.project', + '.settings', + 'build.xml', + 'gen', + 'lint.xml', + 'project.properties']: + self.assertExists('main1', f) + + def test_library_manifest(self): + """Ensure we generate manifest for library projects.""" + self.env = self._consume('android_eclipse', AndroidEclipseBackend) + self.assertExists('library1', 'AndroidManifest.xml') + + def test_classpathentries(self): + """Ensure we produce reasonable classpathentries.""" + self.env = self._consume('android_eclipse', AndroidEclipseBackend) + self.assertExists('main3', '.classpath') + # This is brittle but simple. + with open(mozpath.join(self.env.topobjdir, 'android_eclipse', 'main3', '.classpath'), 'rt') as fh: + lines = fh.readlines() + lines = [line.strip() for line in lines] + self.assertIn('', lines) + self.assertIn('', lines) + self.assertIn('', lines) + + def test_library_project_setting(self): + """Ensure we declare a library project correctly.""" + self.env = self._consume('android_eclipse', AndroidEclipseBackend) + + self.assertExists('library1', 'project.properties') + with open(mozpath.join(self.env.topobjdir, 'android_eclipse', 'library1', 'project.properties'), 'rt') as fh: + lines = fh.readlines() + lines = [line.strip() for line in lines] + self.assertIn('android.library=true', lines) + + self.assertExists('main1', 'project.properties') + with open(mozpath.join(self.env.topobjdir, 'android_eclipse', 'main1', 'project.properties'), 'rt') as fh: + lines = fh.readlines() + lines = [line.strip() for line in lines] + self.assertNotIn('android.library=true', lines) + + def test_referenced_projects(self): + """Ensure we reference another project correctly.""" + self.env = self._consume('android_eclipse', AndroidEclipseBackend) + self.assertExists('main4', '.classpath') + # This is brittle but simple. + with open(mozpath.join(self.env.topobjdir, 'android_eclipse', 'main4', '.classpath'), 'rt') as fh: + lines = fh.readlines() + lines = [line.strip() for line in lines] + self.assertIn('', lines) + + def test_included_projects(self): + """Ensure we include another project correctly.""" + self.env = self._consume('android_eclipse', AndroidEclipseBackend) + self.assertExists('main4', 'project.properties') + # This is brittle but simple. + with open(mozpath.join(self.env.topobjdir, 'android_eclipse', 'main4', 'project.properties'), 'rt') as fh: + lines = fh.readlines() + lines = [line.strip() for line in lines] + self.assertIn('android.library.reference.1=library2', lines) + + def assertInManifest(self, project_name, *args): + manifest_path = mozpath.join(self.env.topobjdir, 'android_eclipse', '%s.manifest' % project_name) + manifest = InstallManifest(manifest_path) + for arg in args: + self.assertIn(arg, manifest, '%s in manifest for project %s' % (arg, project_name)) + + def assertNotInManifest(self, project_name, *args): + manifest_path = mozpath.join(self.env.topobjdir, 'android_eclipse', '%s.manifest' % project_name) + manifest = InstallManifest(manifest_path) + for arg in args: + self.assertNotIn(arg, manifest, '%s not in manifest for project %s' % (arg, project_name)) + + def test_manifest_main_manifest(self): + """Ensure we symlink manifest if asked to for main projects.""" + self.env = self._consume('android_eclipse', AndroidEclipseBackend) + self.assertInManifest('main1', 'AndroidManifest.xml') + + def test_manifest_res(self): + """Ensure we symlink res/ iff asked to.""" + self.env = self._consume('android_eclipse', AndroidEclipseBackend) + self.assertInManifest('library1', 'res') + self.assertNotInManifest('library2', 'res') + + def test_manifest_classpathentries(self): + """Ensure we symlink classpathentries correctly.""" + self.env = self._consume('android_eclipse', AndroidEclipseBackend) + self.assertInManifest('main3', 'a/a', 'b', 'd/e') + + def test_manifest_assets(self): + """Ensure we symlink assets/ iff asked to.""" + self.env = self._consume('android_eclipse', AndroidEclipseBackend) + self.assertNotInManifest('main1', 'assets') + self.assertInManifest('main2', 'assets') + + def test_manifest_extra_jars(self): + """Ensure we symlink extra jars iff asked to.""" + self.env = self._consume('android_eclipse', AndroidEclipseBackend) + self.assertNotInManifest('main1', 'libs') + self.assertInManifest('main2', 'libs/extra.jar') + + +if __name__ == '__main__': + main() diff --git a/python/mozbuild/mozbuild/test/backend/test_recursivemake.py b/python/mozbuild/mozbuild/test/backend/test_recursivemake.py index dd151c3c6e2a..91ea41d5e919 100644 --- a/python/mozbuild/mozbuild/test/backend/test_recursivemake.py +++ b/python/mozbuild/mozbuild/test/backend/test_recursivemake.py @@ -616,6 +616,39 @@ class TestRecursiveMakeBackend(BackendTester): self.assertIn('JAR_MANIFEST := %s/jar.mn' % env.topsrcdir, lines) + def test_android_eclipse(self): + env = self._consume('android_eclipse', RecursiveMakeBackend) + + with open(mozpath.join(env.topobjdir, 'backend.mk'), 'rb') as fh: + lines = fh.readlines() + + lines = [line.rstrip() for line in lines] + + # Dependencies first. + self.assertIn('ANDROID_ECLIPSE_PROJECT_main1: target1 target2', lines) + self.assertIn('ANDROID_ECLIPSE_PROJECT_main4: target3 target4', lines) + + command_template = '\t$(call py_action,process_install_manifest,' + \ + '--no-remove --no-remove-all-directory-symlinks ' + \ + '--no-remove-empty-directories %s %s.manifest)' + # Commands second. + for project_name in ['main1', 'main2', 'library1', 'library2']: + stem = '%s/android_eclipse/%s' % (env.topobjdir, project_name) + self.assertIn(command_template % (stem, stem), lines) + + # Projects declared in subdirectories. + with open(mozpath.join(env.topobjdir, 'subdir', 'backend.mk'), 'rb') as fh: + lines = fh.readlines() + + lines = [line.rstrip() for line in lines] + + self.assertIn('ANDROID_ECLIPSE_PROJECT_submain: subtarget1 subtarget2', lines) + + for project_name in ['submain', 'sublibrary']: + # Destination and install manifest are relative to topobjdir. + stem = '%s/android_eclipse/%s' % (env.topobjdir, project_name) + self.assertIn(command_template % (stem, stem), lines) + if __name__ == '__main__': main()