1#!/usr/bin/env python3
2#
3# Copyright 2017 - The Android Open Source Project
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.
16from distutils import cmd
17from distutils import log
18import subprocess
19import os
20import shutil
21import setuptools
22from setuptools.command import test
23import sys
24
25install_requires = [
26    'backoff',
27    # Future needs to have a newer version that contains urllib.
28    'future>=0.16.0',
29    # Latest version of mock (4.0.0b) causes a number of compatibility issues with ACTS unit tests
30    # b/148695846, b/148814743
31    'mock==3.0.5',
32    # b/157117302: python3.5 is not supported by NumPy 1.19+
33    'numpy<=1.18.1',
34    # b/157117302: python3.5 is not supported by SciPy 1.5.0+ (Monsoon dependency)
35    'scipy==1.4.1',
36    'pyserial',
37    'pyyaml>=5.1',
38    'protobuf>=3.11.3',
39    'retry',
40    'requests',
41    'scapy',
42    'pylibftdi',
43    'xlsxwriter',
44    'mobly>=1.10.0',
45    'grpcio',
46    'Monsoon',
47    # paramiko-ng is needed vs paramiko as currently paramiko does not support
48    # ed25519 ssh keys, which is what Fuchsia uses.
49    'paramiko-ng',
50    'dlipower'
51]
52
53if sys.version_info < (3, ):
54    install_requires.append('enum34')
55    install_requires.append('statistics')
56    # "futures" is needed for py2 compatibility and it only works in 2.7
57    install_requires.append('futures')
58    install_requires.append('py2-ipaddress')
59    install_requires.append('subprocess32')
60
61DEV_PACKAGES = [
62    'shiv'
63]
64
65framework_dir = os.path.dirname(os.path.realpath(__file__))
66
67
68class PyTest(test.test):
69    """Class used to execute unit tests using PyTest. This allows us to execute
70    unit tests without having to install the package.
71    """
72    def finalize_options(self):
73        test.test.finalize_options(self)
74        self.test_args = ['-x', "tests"]
75        self.test_suite = True
76
77    def run_tests(self):
78        test_path = os.path.join(framework_dir, '../tests/meta/ActsUnitTest.py')
79        result = subprocess.Popen('python3 %s' % test_path,
80                                  stdout=sys.stdout,
81                                  stderr=sys.stderr,
82                                  shell=True)
83        result.communicate()
84        sys.exit(result.returncode)
85
86
87class ActsInstallDependencies(cmd.Command):
88    """Installs only required packages
89
90    Installs all required packages for acts to work. Rather than using the
91    normal install system which creates links with the python egg, pip is
92    used to install the packages.
93    """
94
95    description = 'Install dependencies needed for acts to run on this machine.'
96    user_options = []
97
98    def initialize_options(self):
99        pass
100
101    def finalize_options(self):
102        pass
103
104    def run(self):
105        install_args = [sys.executable, '-m', 'pip', 'install']
106        subprocess.check_call(install_args + ['--upgrade', 'pip'])
107        required_packages = self.distribution.install_requires
108
109        for package in required_packages:
110            self.announce('Installing %s...' % package, log.INFO)
111            subprocess.check_call(install_args +
112                                  ['-v', '--no-cache-dir', package])
113
114        self.announce('Dependencies installed.')
115
116
117class ActsUninstall(cmd.Command):
118    """Acts uninstaller.
119
120    Uninstalls acts from the current version of python. This will attempt to
121    import acts from any of the python egg locations. If it finds an import
122    it will use the modules file location to delete it. This is repeated until
123    acts can no longer be imported and thus is uninstalled.
124    """
125
126    description = 'Uninstall acts from the local machine.'
127    user_options = []
128
129    def initialize_options(self):
130        pass
131
132    def finalize_options(self):
133        pass
134
135    def uninstall_acts_module(self, acts_module):
136        """Uninstalls acts from a module.
137
138        Args:
139            acts_module: The acts module to uninstall.
140        """
141        for acts_install_dir in acts_module.__path__:
142            self.announce('Deleting acts from: %s' % acts_install_dir,
143                          log.INFO)
144            shutil.rmtree(acts_install_dir)
145
146    def run(self):
147        """Entry point for the uninstaller."""
148        # Remove the working directory from the python path. This ensures that
149        # Source code is not accidentally targeted.
150        if framework_dir in sys.path:
151            sys.path.remove(framework_dir)
152
153        if os.getcwd() in sys.path:
154            sys.path.remove(os.getcwd())
155
156        try:
157            import acts as acts_module
158        except ImportError:
159            self.announce('Acts is not installed, nothing to uninstall.',
160                          level=log.ERROR)
161            return
162
163        while acts_module:
164            self.uninstall_acts_module(acts_module)
165            try:
166                del sys.modules['acts']
167                import acts as acts_module
168            except ImportError:
169                acts_module = None
170
171        self.announce('Finished uninstalling acts.')
172
173
174def main():
175    scripts = [
176        os.path.join('acts', 'bin', 'act.py'),
177        os.path.join('acts', 'bin', 'monsoon.py')
178    ]
179    # cd to framework directory so the correct package namespace is found
180    os.chdir(framework_dir)
181    setuptools.setup(name='acts',
182                     version='0.9',
183                     description='Android Comms Test Suite',
184                     license='Apache2.0',
185                     packages=setuptools.find_packages(),
186                     include_package_data=True,
187                     tests_require=['pytest'],
188                     install_requires=install_requires,
189                     extras_require={'dev': DEV_PACKAGES},
190                     scripts=scripts,
191                     cmdclass={
192                         'test': PyTest,
193                         'install_deps': ActsInstallDependencies,
194                         'uninstall': ActsUninstall
195                     },
196                     url="http://www.android.com/")
197
198    if {'-u', '--uninstall', 'uninstall'}.intersection(sys.argv):
199        installed_scripts = [
200            '/usr/local/bin/act.py', '/usr/local/bin/monsoon.py'
201        ]
202        for act_file in installed_scripts:
203            if os.path.islink(act_file):
204                os.unlink(act_file)
205            elif os.path.exists(act_file):
206                os.remove(act_file)
207
208
209if __name__ == '__main__':
210    main()
211