├── uberpoet ├── resources │ ├── tools │ │ └── rules │ │ │ ├── BUILD.bazel │ │ │ └── repository_rules.bzl │ ├── get_market_name.sh │ ├── mockbuckconfig │ ├── cpu_log.sh │ ├── mockpodfile │ ├── mockbucklibtemplate.bzl │ ├── mockbazellibtemplate.bzl │ ├── mockbuckapptemplate.bzl │ ├── mockcplibtemplate.podspec │ ├── mockcpapptemplate.podspec │ ├── Info.plist │ ├── mockbazelapptemplate.bzl │ ├── mockappdelegate │ └── mockbazelworkspace ├── __init__.py ├── memoize.py ├── locreader.py ├── loccalc.py ├── util.py ├── moduletree.py ├── cpulogger.py ├── statemanagement.py ├── genproj.py ├── commandlineutil.py ├── blazeprojectgen.py ├── cpprojectgen.py └── filegen.py ├── SECURITY.md ├── docs ├── images │ ├── flat.png │ ├── bs_flat.png │ ├── layered_3.png │ ├── project_gen.png │ ├── bs_layered_simple.png │ ├── note.txt │ └── project_gen.xml ├── layer_types.md └── CONTRIBUTING.md ├── release_notes.md ├── Pipfile ├── setup.cfg ├── tests ├── fixtures │ ├── cloc_out.json │ ├── cpu_log.txt │ └── loc_mappings.json ├── __init__.py ├── test_dotreader.py ├── utils.py ├── test_locreader.py ├── test_loccalc.py ├── test_cpulogger.py ├── test_moduletree.py ├── test_filegen.py ├── test_statemanagement.py ├── test_buck_integration.py ├── test_cp_integration.py └── test_bazel_integration.py ├── genproj.py ├── multisuite.py ├── examples ├── buckgenproj_test.sh ├── bazelgenproj_test.sh ├── cpgenproj_test.sh └── multisuite_test.sh ├── .github └── workflows │ └── python-app.yml ├── setup.py ├── .gitignore ├── README.md └── LICENSE.txt /uberpoet/resources/tools/rules/BUILD.bazel: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | Just file an issue in github. 4 | -------------------------------------------------------------------------------- /docs/images/flat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uber/uber-poet/HEAD/docs/images/flat.png -------------------------------------------------------------------------------- /docs/images/bs_flat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uber/uber-poet/HEAD/docs/images/bs_flat.png -------------------------------------------------------------------------------- /release_notes.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | ## 0.9.0 4 | 5 | * Initial pre-public release version -------------------------------------------------------------------------------- /docs/images/layered_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uber/uber-poet/HEAD/docs/images/layered_3.png -------------------------------------------------------------------------------- /docs/images/project_gen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uber/uber-poet/HEAD/docs/images/project_gen.png -------------------------------------------------------------------------------- /docs/images/bs_layered_simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uber/uber-poet/HEAD/docs/images/bs_layered_simple.png -------------------------------------------------------------------------------- /docs/images/note.txt: -------------------------------------------------------------------------------- 1 | The xml file(s) are the file format of graphs created with https://www.draw.io/ . Go there to edit these graphs, export them as PNG files and put the updated files in here. 2 | 3 | 4 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | typing = "==3.6.6" 8 | toposort = "==1.6.0" 9 | 10 | [dev-packages] 11 | mock = "==2.0.0" 12 | testfixtures = "==6.3.0" 13 | pytest = "==4.0.0" 14 | pytest-cov = "==2.6.0" 15 | "flake8" = "==3.6.0" 16 | yapf = "==0.25.0" 17 | isort = "==4.3.4" 18 | future = "*" 19 | 20 | [requires] 21 | python_version = "2.7" 22 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | exclude = build, dist, env 4 | 5 | [isort] 6 | line_length = 120 7 | not_skip = __init__.py 8 | known_first_party = uberpoet, tests 9 | 10 | [yapf] 11 | based_on_style = google 12 | column_limit = 120 13 | 14 | # Since coverage interferes with debugging, we leave this commented out by default 15 | # [tool:pytest] 16 | # addopts = --cov=uberpoet --cov-report xml:cov.xml --cov-report term-missing -------------------------------------------------------------------------------- /tests/fixtures/cloc_out.json: -------------------------------------------------------------------------------- 1 | {"header" : { 2 | "cloc_url" : "github.com/AlDanial/cloc", 3 | "cloc_version" : "1.78", 4 | "elapsed_seconds" : 1.01171088218689, 5 | "n_files" : 1, 6 | "n_lines" : 160, 7 | "files_per_second" : 0.988424675079529, 8 | "lines_per_second" : 158.147948012725}, 9 | "Swift" :{ 10 | "nFiles": 1, 11 | "blank": 28, 12 | "comment": 0, 13 | "code": 132}, 14 | "SUM": { 15 | "blank": 28, 16 | "comment": 0, 17 | "code": 132, 18 | "nFiles": 1} } -------------------------------------------------------------------------------- /tests/fixtures/cpu_log.txt: -------------------------------------------------------------------------------- 1 | 1535510161 CPU usage: 9.12% user, 18.24% sys, 72.62% idle 2 | 1535510162 CPU usage: 5.51% user, 16.78% sys, 77.69% idle 3 | 1535510164 CPU usage: 9.17% user, 18.59% sys, 72.22% idle 4 | 1535510165 CPU usage: 5.64% user, 15.60% sys, 78.75% idle 5 | 1535510166 CPU usage: 4.79% user, 14.97% sys, 80.23% idle 6 | 1535510167 CPU usage: 7.77% user, 20.21% sys, 72.0% idle 7 | 1535510168 CPU usage: 14.54% user, 17.90% sys, 67.54% idle 8 | 1535510169 CPU usage: 4.70% user, 17.94% sys, 77.35% idle 9 | 1535510170 CPU usage: 3.11% user, 12.35% sys, 84.53% idle 10 | 1535510171 CPU usage: 5.99% user, 17.26% sys, 76.73% idle -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Uber Technologies, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /uberpoet/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Uber Technologies, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name = "uberpoet" 16 | -------------------------------------------------------------------------------- /genproj.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2018 Uber Technologies, Inc. 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 | from uberpoet.genproj import GenProjCommandLine 18 | 19 | if __name__ == "__main__": 20 | GenProjCommandLine().main() 21 | -------------------------------------------------------------------------------- /uberpoet/resources/get_market_name.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright (c) 2018 Uber Technologies, Inc. 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 | defaults read "$HOME/Library/Preferences/com.apple.SystemProfiler.plist" 'CPU Names' | cut -sd '"' -f 4 | uniq 18 | -------------------------------------------------------------------------------- /multisuite.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2018 Uber Technologies, Inc. 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 | from uberpoet.multisuite import CommandLineMultisuite 18 | 19 | if __name__ == "__main__": 20 | CommandLineMultisuite().main() 21 | -------------------------------------------------------------------------------- /uberpoet/resources/mockbuckconfig: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Uber Technologies, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # This code was @generated by Uber Poet, a mock application generator. 16 | # Check it out at https://github.com/uber/uber-poet 17 | 18 | [cxx] 19 | default_platform = iphonesimulator-x86_64 20 | 21 | [swift] 22 | version = 5 23 | 24 | [project] 25 | ide = xcode 26 | -------------------------------------------------------------------------------- /tests/test_dotreader.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from uberpoet.dotreader import DotFileReader 5 | from uberpoet.moduletree import ModuleNode 6 | 7 | 8 | class TestDotReader(unittest.TestCase): 9 | 10 | def test_read_integration(self): 11 | test_fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures', 'test_dot.gv') 12 | main_root_name = "DotReaderMainModule" 13 | dr = DotFileReader() 14 | root, nodes = dr.read_dot_file(test_fixture_path, main_root_name, is_debug=True) 15 | 16 | self.assertEqual(root.name, main_root_name) 17 | self.assertEqual(len(nodes), 338) 18 | self.assertEqual(root.node_type, ModuleNode.APP) 19 | self.assertIn(root, nodes) 20 | self.assertEqual(len(root.deps), 1) 21 | self.assertEqual(len(root.deps[0].deps[0].deps), 5) 22 | 23 | for node in nodes: 24 | if node.name != main_root_name: 25 | self.assertEqual(node.node_type, ModuleNode.LIBRARY) 26 | -------------------------------------------------------------------------------- /uberpoet/resources/cpu_log.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (c) 2018 Uber Technologies, Inc. 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 | date_fn () { 18 | while read -r LINE 19 | do 20 | echo "$(date +%s) $LINE" 21 | done 22 | } 23 | 24 | # This isn't a perfect way to track cpu usage. 25 | # You'll notice some skew in terms of a second or two, 26 | # but it works well enough for our usecase of 700-1500s builds. 27 | 28 | top -l 0 -n 0 | grep --line-buffered CPU | date_fn -------------------------------------------------------------------------------- /uberpoet/resources/tools/rules/repository_rules.bzl: -------------------------------------------------------------------------------- 1 | """ 2 | Repository rules used in the WORKSPACE. 3 | """ 4 | 5 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 6 | 7 | def github_repo(name, project, repo, ref, sha256 = None): 8 | github_url = "https://github.com/%s/%s/archive/%s.zip" % (project, repo, ref) 9 | http_archive( 10 | name = name, 11 | strip_prefix = "%s-%s" % (repo, ref.replace("/", "-")), 12 | url = github_url, 13 | sha256 = sha256, 14 | canonical_id = github_url, 15 | ) 16 | 17 | def check_execute(repository_ctx, *args, **kwargs): 18 | exec_result = repository_ctx.execute(*args, **kwargs) 19 | if exec_result.return_code != 0: 20 | fail("{}: executing {} {} failed {}:\n{}\n{}".format( 21 | repository_ctx.name, 22 | args, 23 | kwargs, 24 | exec_result.return_code, 25 | exec_result.stdout, 26 | exec_result.stderr, 27 | ).rstrip()) 28 | return exec_result 29 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Uber Technologies, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | 17 | 18 | def do_nothing(_): 19 | pass 20 | 21 | 22 | def integration_test(func): 23 | if 'INTEGRATION' in os.environ: 24 | return func 25 | else: 26 | return do_nothing 27 | 28 | 29 | def read_file(path): 30 | with open(path, 'r') as f: 31 | return f.read() 32 | 33 | 34 | def write_file(path, text): 35 | with open(path, 'w') as f: 36 | f.write(text) 37 | -------------------------------------------------------------------------------- /tests/test_locreader.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from uberpoet.filegen import Language 5 | from uberpoet.locreader import LocFileReader 6 | 7 | 8 | class TestLocReader(unittest.TestCase): 9 | 10 | def test_loc_for_module(self): 11 | test_fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures', 'loc_mappings.json') 12 | lr = LocFileReader() 13 | lr.read_loc_file(test_fixture_path) 14 | 15 | self.assertEqual(419, lr.loc_for_module("DotReaderLib16")) 16 | 17 | def test_language_for_module(self): 18 | test_fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures', 'loc_mappings.json') 19 | lr = LocFileReader() 20 | lr.read_loc_file(test_fixture_path) 21 | 22 | self.assertEqual(Language.OBJC, lr.language_for_module("DotReaderLib0")) 23 | self.assertEqual(Language.SWIFT, lr.language_for_module("DotReaderLib16")) 24 | 25 | def test_throws_exception_without_read(self): 26 | lr = LocFileReader() 27 | self.assertRaises(ValueError, lr.loc_for_module, "DotReaderLib16") 28 | -------------------------------------------------------------------------------- /tests/test_loccalc.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Uber Technologies, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import unittest 16 | 17 | from uberpoet.filegen import Language 18 | from uberpoet.loccalc import LOCCalculator 19 | 20 | 21 | class TestLOCCalculator(unittest.TestCase): 22 | 23 | def test_calculate_loc(self): 24 | loc_calc = LOCCalculator() 25 | self.assertEqual(loc_calc.calculate_loc('body', Language.SWIFT), 1) 26 | 27 | def test_calculate_loc_multiple_lines(self): 28 | loc_calc = LOCCalculator() 29 | self.assertEqual(loc_calc.calculate_loc('body\ntext', Language.SWIFT), 2) 30 | -------------------------------------------------------------------------------- /uberpoet/resources/mockpodfile: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Uber Technologies, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # This code was @generated by Uber Poet, a mock application generator. 16 | # Check it out at https://github.com/uber/uber-poet 17 | 18 | platform :ios, '12.0' 19 | use_frameworks!(:linkage => {2}) 20 | inhibit_all_warnings! 21 | 22 | install! 'cocoapods', 23 | integrate_targets: false, 24 | deterministic_uuids: {3}, 25 | generate_multiple_pod_projects: {4} 26 | 27 | target 'App' do 28 | # app spec integration 29 | {0} 30 | 31 | # local dependencies 32 | {1} 33 | end 34 | -------------------------------------------------------------------------------- /examples/buckgenproj_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (c) 2018 Uber Technologies, Inc. 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 | set -xe 18 | 19 | GIT_ROOT="$(git rev-parse --show-toplevel)" 20 | GENPROJ_ROOT="$HOME/Desktop/buckgenproj_out" 21 | BUILD_LOG_PATH="$GENPROJ_ROOT/mockapp_build_log.txt" 22 | PROJECT_OUT="$GENPROJ_ROOT/mockapp" 23 | 24 | xcodebuild -version 25 | pipenv install 26 | 27 | pipenv run $GIT_ROOT/genproj.py \ 28 | --output_directory "$PROJECT_OUT" \ 29 | --blaze_module_path "/mockapp" \ 30 | --gen_type flat \ 31 | --swift_lines_of_code 150000 32 | 33 | MOCK_WORKSPACE_PATH="$PROJECT_OUT/App/App.xcworkspace" 34 | cd "$PROJECT_OUT" 35 | time buck build "//..." > "$BUILD_LOG_PATH" 36 | -------------------------------------------------------------------------------- /examples/bazelgenproj_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (c) 2021 Uber Technologies, Inc. 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 | set -xe 18 | 19 | GIT_ROOT="$(git rev-parse --show-toplevel)" 20 | GENPROJ_ROOT="$HOME/Desktop/bazelgenproj_out" 21 | BUILD_LOG_PATH="$GENPROJ_ROOT/mockapp_build_log.txt" 22 | PROJECT_OUT="$GENPROJ_ROOT/mockapp" 23 | 24 | xcodebuild -version 25 | pipenv install 26 | 27 | pipenv run $GIT_ROOT/genproj.py \ 28 | --output_directory "$PROJECT_OUT" \ 29 | --project_generator_type "bazel" \ 30 | --blaze_module_path "/mockapp" \ 31 | --gen_type flat \ 32 | --swift_lines_of_code 150000 33 | 34 | cd "$PROJECT_OUT" 35 | time bazel build "//..." --incompatible_require_linker_input_cc_api=false > "$BUILD_LOG_PATH" 36 | -------------------------------------------------------------------------------- /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Build and Run Tests 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build-and-test: 14 | 15 | runs-on: macos-latest 16 | env: 17 | INTEGRATION: 1 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Set up Python 2.7 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: 2.7 24 | - name: Install all development dependencies with pipenv 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install pipenv 28 | pipenv install --dev 29 | - name: Lint with flake8 30 | run: | 31 | pipenv run flake8 32 | - name: Test formatting compliance with yapf 33 | run: | 34 | pipenv run yapf -rd uberpoet/ test/ *py 35 | - name: Test that imports were sorted with isort 36 | run: | 37 | pipenv run isort -c 38 | - name: Run unit and integration tests with pytest 39 | run: | 40 | pipenv run pytest --cov=uberpoet --cov-report xml:cov.xml --cov-report term-missing 41 | -------------------------------------------------------------------------------- /tests/test_cpulogger.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Uber Technologies, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | import unittest 17 | 18 | from uberpoet.cpulogger import CPULog 19 | 20 | 21 | class TestCPULogger(unittest.TestCase): 22 | 23 | def test_cpu_convert(self): 24 | test_path = os.path.join(os.path.dirname(__file__), 'fixtures', 'cpu_log.txt') 25 | with open(test_path, 'r') as log: 26 | out = [CPULog(line) for line in log] 27 | chrome_out = [i.chrome_trace() for i in out] 28 | first_time = 1535510161 29 | 30 | self.assertEqual(len(out), 10) 31 | self.assertEqual(len(chrome_out), 10) 32 | self.assertEqual(chrome_out[0]['ts'], first_time * CPULog.EPOCH_MULT) 33 | self.assertEqual(out[0].epoch, first_time) 34 | -------------------------------------------------------------------------------- /examples/cpgenproj_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (c) 2021 Uber Technologies, Inc. 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 | set -xe 18 | 19 | GIT_ROOT="$(git rev-parse --show-toplevel)" 20 | GENPROJ_ROOT="$HOME/Desktop/cpgenproj_out" 21 | BUILD_LOG_PATH="$GENPROJ_ROOT/mockapp_build_log.txt" 22 | PROJECT_OUT="$GENPROJ_ROOT/mockapp" 23 | 24 | xcodebuild -version 25 | pipenv install 26 | 27 | pipenv run $GIT_ROOT/genproj.py \ 28 | --output_directory "$PROJECT_OUT" \ 29 | --project_generator_type "cocoapods" \ 30 | --gen_type flat \ 31 | --swift_lines_of_code 150000 32 | 33 | MOCK_WORKSPACE_PATH="$PROJECT_OUT/Pods/Pods.xcodeproj" 34 | cd "$PROJECT_OUT" 35 | pod install 36 | time xcodebuild build -scheme AppContainer-App -sdk iphonesimulator -project "$MOCK_WORKSPACE_PATH" > "$BUILD_LOG_PATH" 37 | -------------------------------------------------------------------------------- /examples/multisuite_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (c) 2018 Uber Technologies, Inc. 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 | set -xe 18 | 19 | GIT_ROOT="$(git rev-parse --show-toplevel)" 20 | 21 | pipenv install 22 | 23 | # You usually want to use `caffeinate` to prevent your computer from going to sleep during 24 | # a multi hour build test suite. 25 | caffeinate -s pipenv run $GIT_ROOT/multisuite.py \ 26 | --log_dir "$HOME/Desktop/multisuite_build_results" \ 27 | --app_gen_output_dir "$HOME/Desktop/multisuite_build_results/app_gen" \ 28 | --test_build_only \ 29 | "$@" 30 | 31 | 32 | # Uncomment this if you use --trace_cpu: 33 | # TODO figure out a better way to catch dangling top processes if there is a crash / error in the main program 34 | # echo "Killing potential dangling top subprocesses from CPULog" 35 | # sudo killall top 36 | -------------------------------------------------------------------------------- /uberpoet/resources/mockbucklibtemplate.bzl: -------------------------------------------------------------------------------- 1 | # BUILD FILE SYNTAX: SKYLARK 2 | # More Info: https://buckbuild.com/concept/skylark.html 3 | 4 | # Copyright (c) 2018 Uber Technologies, Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | # This code was @generated by Uber Poet, a mock application generator. 19 | # Check it out at https://github.com/uber/uber-poet 20 | 21 | apple_library( 22 | name = "{0}", 23 | srcs = glob([ 24 | "Sources/**/*.m", 25 | "Sources/**/*.swift", 26 | ]), 27 | headers = glob([ 28 | "Sources/**/*.h" 29 | ]), 30 | tests = [], 31 | visibility = [ 32 | "PUBLIC", 33 | ], 34 | compiler_flags = ["-fmodules"], 35 | configs = {{ 36 | "Debug": {{ 37 | "SWIFT_WHOLE_MODULE_OPTIMIZATION": "{2}", 38 | }}, 39 | }}, 40 | deps = [ 41 | {1} 42 | ], 43 | ) 44 | -------------------------------------------------------------------------------- /uberpoet/resources/mockbazellibtemplate.bzl: -------------------------------------------------------------------------------- 1 | # BUILD FILE SYNTAX: SKYLARK 2 | 3 | # Copyright (c) 2021 Uber Technologies, Inc. 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 | # This code was @generated by Uber Poet, a mock application generator. 18 | # Check it out at https://github.com/uber/uber-poet 19 | 20 | load("@build_bazel_rules_ios//rules:framework.bzl", "apple_framework") 21 | 22 | apple_framework( 23 | name = "{0}", 24 | srcs = glob([ 25 | "Sources/**/*.h", 26 | "Sources/**/*.m", 27 | "Sources/**/*.swift", 28 | ]), 29 | sdk_frameworks = [ 30 | "Foundation", 31 | "UIKit", 32 | ], 33 | platforms = {{"ios": "12.0"}}, 34 | swift_version = "5.0", 35 | visibility = ["//visibility:public"], 36 | xcconfig = {{ 37 | "SWIFT_WHOLE_MODULE_OPTIMIZATION[config=Debug]": "{2}", 38 | }}, 39 | deps = [ 40 | {1} 41 | ], 42 | ) 43 | -------------------------------------------------------------------------------- /uberpoet/resources/mockbuckapptemplate.bzl: -------------------------------------------------------------------------------- 1 | # BUILD FILE SYNTAX: SKYLARK 2 | # More Info: https://buckbuild.com/concept/skylark.html 3 | 4 | # Copyright (c) 2018 Uber Technologies, Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | # This code was @generated by Uber Poet, a mock application generator. 19 | # Check it out at https://github.com/uber/uber-poet 20 | 21 | apple_bundle( 22 | name = "App", 23 | binary = ":AppBinary", 24 | extension = "app", 25 | info_plist = "Info.plist", 26 | ) 27 | 28 | apple_binary( 29 | name = "AppBinary", 30 | srcs = glob([ 31 | "*.m", 32 | "*.swift", 33 | ]), 34 | headers = glob([ 35 | "*.h" 36 | ]), 37 | linker_flags = ["-ObjC"], 38 | compiler_flags = ["-fmodules"], 39 | configs = {{ 40 | "Debug": {{ 41 | "SWIFT_WHOLE_MODULE_OPTIMIZATION": "{2}", 42 | }}, 43 | }}, 44 | deps = [ 45 | {1} 46 | ], 47 | ) 48 | -------------------------------------------------------------------------------- /uberpoet/resources/mockcplibtemplate.podspec: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Uber Technologies, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # This code was @generated by Uber Poet, a mock application generator. 16 | # Check it out at https://github.com/uber/uber-poet 17 | 18 | Pod::Spec.new do |s| 19 | s.name = '{0}' 20 | s.version = '1.0.0' 21 | s.summary = 'Podspec for {0} library.' 22 | s.authors = 'Generated' 23 | s.homepage = 'https://github.com/uber-research/poet' 24 | s.source = {{ git: 'GENERATED/NOTPUBLISHED', tag: '1.0.0' }} 25 | s.swift_version = '5.0' 26 | 27 | s.pod_target_xcconfig = {{ 'SWIFT_WHOLE_MODULE_OPTIMIZATION[config=Debug]' => '{2}' }} 28 | 29 | s.frameworks = ['Foundation', 'UIKit'] 30 | 31 | s.ios.deployment_target = '12.0' 32 | 33 | s.source_files = 'Sources/*.{{h,m,swift}}' 34 | 35 | # dependencies 36 | {1} 37 | end 38 | -------------------------------------------------------------------------------- /uberpoet/resources/mockcpapptemplate.podspec: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Uber Technologies, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # This code was @generated by Uber Poet, a mock application generator. 16 | # Check it out at https://github.com/uber/uber-poet 17 | 18 | Pod::Spec.new do |s| 19 | s.name = 'AppContainer' 20 | s.version = '1.0.0' 21 | s.summary = 'Podspec for app.' 22 | s.homepage = 'https://github.com/uber-research/poet' 23 | s.authors = 'Generated' 24 | s.source = {{ git: 'GENERATED/NOTPUBLISHED', tag: '1.0.0' }} 25 | s.swift_version = '5.0' 26 | 27 | s.source_files = 'dummy.swift' 28 | 29 | s.ios.deployment_target = '12.0' 30 | 31 | s.app_spec 'App' do |app_spec| 32 | app_spec.pod_target_xcconfig = {{ 'SWIFT_WHOLE_MODULE_OPTIMIZATION[config=Debug]' => '{1}' }} 33 | app_spec.frameworks = ['Foundation', 'UIKit'] 34 | 35 | app_spec.source_files = '*.swift' 36 | 37 | # dependencies 38 | {0} 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /uberpoet/memoize.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Uber Technologies, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import collections 16 | import functools 17 | 18 | 19 | # See https://wiki.python.org/moin/PythonDecoratorLibrary#Memoize 20 | class memoized(object): 21 | '''Decorator. Caches a function's return value each time it is called. 22 | If called later with the same arguments, the cached value is returned 23 | (not reevaluated). 24 | ''' 25 | 26 | def __init__(self, func): 27 | self.func = func 28 | self.cache = {} 29 | 30 | def __call__(self, *args): 31 | if not isinstance(args, collections.Hashable): 32 | # uncacheable. a list, for instance. 33 | # better to not cache than blow up. 34 | return self.func(*args) 35 | if args in self.cache: 36 | return self.cache[args] 37 | else: 38 | value = self.func(*args) 39 | self.cache[args] = value 40 | return value 41 | 42 | def __repr__(self): 43 | '''Return the function's docstring.''' 44 | return self.func.__doc__ 45 | 46 | def __get__(self, obj, objtype): 47 | '''Support instance methods.''' 48 | return functools.partial(self.__call__, obj) 49 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Uber Technologies, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import setuptools 16 | 17 | with open("README.md", "r") as fh: 18 | long_description = fh.read() 19 | 20 | setuptools.setup( 21 | name="uberpoet", 22 | version="1.0.0", 23 | license='Apache License, Version 2.0', 24 | author="Uber Technologies, Inc.", 25 | description="A mock swift project generator & build runner to help benchmark various module configurations.", 26 | long_description=long_description, 27 | long_description_content_type="text/markdown", 28 | url="https://github.com/uber/uber-poet", 29 | packages=setuptools.find_packages(), 30 | classifiers=[ 31 | "Programming Language :: Python :: 2.7", 32 | "License :: OSI Approved :: Apache Software License", 33 | "Operating System :: MacOS :: MacOS X", 34 | "Development Status :: 4 - Beta", 35 | "Environment :: Console", 36 | "Environment :: MacOS X", 37 | "Intended Audience :: Developers", 38 | "Topic :: Software Development :: Code Generators", 39 | ], 40 | entry_points={ 41 | 'console_scripts': [ 42 | 'uberpoet-genproj.py=uberpoet.genproj:main', 43 | 'uberpoet-multisuite.py=uberpoet.multisuite:main', 44 | ], 45 | }, 46 | ) 47 | -------------------------------------------------------------------------------- /uberpoet/resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Uber Mock App 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | com.uber.UberMockApp 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleLocalizations 16 | 17 | en 18 | 19 | CFBundleName 20 | MockApp 21 | CFBundlePackageType 22 | APPL 23 | CFBundleShortVersionString 24 | 4.101.0 25 | CFBundleSignature 26 | ???? 27 | CFBundleSupportedPlatforms 28 | 29 | iPhoneOS 30 | 31 | CFBundleURLTypes 32 | 33 | 34 | CFBundleURLName 35 | com.uber.mock.app 36 | CFBundleURLSchemes 37 | 38 | ubermockapp 39 | 40 | 41 | 42 | CFBundleVersion 43 | 4.101.0 44 | LSRequiresIPhoneOS 45 | 46 | MinimumOSVersion 47 | 9.0 48 | UILaunchStoryboardName 49 | MockApp 50 | UIRequiredDeviceCapabilities 51 | 52 | armv7 53 | 54 | UIRequiresFullScreen 55 | 56 | UIStatusBarHidden 57 | 58 | UISupportedInterfaceOrientations 59 | 60 | UIInterfaceOrientationPortrait 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /tests/test_moduletree.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Uber Technologies, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import unittest 16 | 17 | from toposort import toposort_flatten 18 | 19 | from uberpoet.moduletree import ModuleNode 20 | 21 | 22 | class TestModuleTree(unittest.TestCase): 23 | 24 | def test_gen_layered_graph(self): 25 | root, nodes = ModuleNode.gen_layered_graph(10, 10) 26 | self.verify_graph(nodes) 27 | self.assertEqual(len(nodes), 10 * 10 + 1) 28 | self.assertEqual(ModuleNode.APP, root.node_type) 29 | 30 | def test_gen_bs_layered_graph(self): 31 | root, nodes = ModuleNode.gen_layered_big_small_graph(10, 10) 32 | self.verify_graph(nodes) 33 | self.assertEqual(len(nodes), 19 + 1) 34 | self.assertEqual(ModuleNode.APP, root.node_type) 35 | 36 | def verify_graph(self, nodes): 37 | # The generated layered graphs add dependencies randomly to modules within each layer. 38 | # Because of that, we cannot always specify a fixed expected list of nodes without making 39 | # tests indeterministic. Instead, we verify topological sorting by re-creating the same graph, 40 | # sorting it topologicaly and assert the two lists to be the same. 41 | graph = {n: set(n.deps) for n in nodes} 42 | self.assertEqual(toposort_flatten(graph), nodes) 43 | -------------------------------------------------------------------------------- /uberpoet/resources/mockbazelapptemplate.bzl: -------------------------------------------------------------------------------- 1 | # BUILD FILE SYNTAX: SKYLARK 2 | # More Info: https://buckbuild.com/concept/skylark.html 3 | 4 | # Copyright (c) 2021 Uber Technologies, Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | # This code was @generated by Uber Poet, a mock application generator. 19 | # Check it out at https://github.com/uber/uber-poet 20 | 21 | load("@build_bazel_rules_ios//rules:app.bzl", "ios_application") 22 | load("@build_bazel_rules_ios//rules:framework.bzl", "apple_framework") 23 | 24 | apple_framework( 25 | name = "AppContainer", 26 | srcs = glob([ 27 | "Sources/**/*.h", 28 | "Sources/**/*.m", 29 | "Sources/**/*.swift", 30 | ]), 31 | platforms = {{"ios": "12.0"}}, 32 | swift_version = "5.0", 33 | visibility = ["//visibility:public"], 34 | deps = [ 35 | {1} 36 | ], 37 | ) 38 | 39 | ios_application( 40 | name = "App", 41 | srcs = glob([ 42 | "*.h", 43 | "*.m", 44 | "*.swift", 45 | ]), 46 | bundle_id = "com.uber.UberMockApp", 47 | families = [ 48 | "iphone", 49 | "ipad", 50 | ], 51 | infoplists = ["Info.plist"], 52 | minimum_os_version = "12.0", 53 | sdk_frameworks = [ 54 | "Foundation", 55 | "UIKit", 56 | ], 57 | linkopts = [ 58 | "-ObjC" 59 | ], 60 | swift_version = "5.0", 61 | xcconfig = {{ 62 | "SWIFT_WHOLE_MODULE_OPTIMIZATION": "{2}", 63 | }}, 64 | deps = [":AppContainer"], 65 | ) 66 | -------------------------------------------------------------------------------- /uberpoet/locreader.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Uber Technologies, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from __future__ import absolute_import 16 | 17 | import json 18 | from typing import Dict, List, NoReturn, Set # noqa: F401 19 | 20 | from .filegen import Language 21 | 22 | 23 | class LocFileReader(object): 24 | """ 25 | This class reads a JSON file that includes the LOC information for each module and it supplies it to the 26 | project generator. 27 | """ 28 | 29 | def __init__(self): 30 | self.cloc_mappings = None 31 | 32 | def read_loc_file(self, loc_file_path): 33 | # type: (str) -> NoReturn 34 | with open(loc_file_path, 'r') as f: 35 | self.cloc_mappings = json.load(f) 36 | 37 | def loc_for_module(self, mod_name): 38 | # type: (str) -> int 39 | if self.cloc_mappings is None: 40 | raise ValueError("Unable to provide LOC for module {}, no data is loaded yet!".format(mod_name)) 41 | module_info = self.cloc_mappings[mod_name] 42 | return module_info["loc"] if type(module_info) is dict else module_info 43 | 44 | def language_for_module(self, mod_name): 45 | # type: (str) -> str 46 | if self.cloc_mappings is None: 47 | raise ValueError("Unable to provide LOC for module {}, no data is loaded yet!".format(mod_name)) 48 | module_info = self.cloc_mappings[mod_name] 49 | return module_info["language"] if type(module_info) is dict else Language.SWIFT 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # from https://github.com/github/gitignore/blob/master/Python.gitignore 2 | # which was under a CC0 licence / public domain 3 | 4 | # vscode 5 | .vscode 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | cov.xml 54 | 55 | *.cover 56 | .hypothesis/ 57 | .pytest_cache/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | .python-version 90 | 91 | # celery beat schedule file 92 | celerybeat-schedule 93 | 94 | # SageMath parsed files 95 | *.sage.py 96 | 97 | # Environments 98 | .env 99 | .venv 100 | env/ 101 | venv/ 102 | ENV/ 103 | env.bak/ 104 | venv.bak/ 105 | 106 | # Spyder project settings 107 | .spyderproject 108 | .spyproject 109 | 110 | # Rope project settings 111 | .ropeproject 112 | 113 | # mkdocs documentation 114 | /site 115 | 116 | # mypy 117 | .mypy_cache/ 118 | .dmypy.json 119 | dmypy.json 120 | 121 | # Pyre type checker 122 | .pyre/ 123 | -------------------------------------------------------------------------------- /docs/layer_types.md: -------------------------------------------------------------------------------- 1 | # Module Generation Graph Types 2 | 3 | ## flat 4 | ![a flat dependency graph](images/flat.png) 5 | 6 | A module graph that has one app module which depends on one library layer of `module_count` modules. Those modules do not depend on anything. 7 | 8 | ## bs_flat ("Big Small Flat") 9 | ![a flat dependency graph with big and small nodes](images/bs_flat.png) 10 | 11 | A module graph that has one app module which depends on one library layer of modules. Those modules do not depend on anything. Some of the modules in the library layer are 'big', the rest are small. There are `big_module_count` number of big modules, and `small_module_count` small modules. 12 | 13 | ## layered 14 | ![a layered dependency graph](images/layered_3.png) 15 | 16 | A module graph of one app module and `app_layer_count` number of library layers. Each layer depends on a random selection of modules in lower layers. There are `module_count` number of library modules. 17 | 18 | Due to probability a random selection of modules won't be built because they won't be connected to main module graph if the module count is large enough, since each module in a layered tree only connects to 5 other modules in a large set. 19 | 20 | ## bs_layered ("Big Small Layered") 21 | ![a simplified big small layered dependency graph](images/bs_layered_simple.png) 22 | 23 | A module graph that has one app module, one flat layer of `big_module_count` big modules and `layer_count` layers of `small_module_count` modules under the big module layer which connect to random modules inside like the layed module graph type. 24 | 25 | Due to probability a random selection of modules won't be built because they won't be connected to main module graph if the module count is large enough, since each module in a layered tree only connects to 5 other modules in a large set. 26 | 27 | ## dot 28 | Reads a dot file specified at `dot_file_path` which represents a dependency graph of code modules. Picks `dot_root_node_name` as the app node to generate the app from. You can generate a dot graph of your own buck app by using something like `buck query "deps(//apps/myapp:App)" --dot > file.gv`. Every module in a dot graph mock app is the same size, unlike most applicaitons. Future improvements could co-relate this dot graph with a lines of code file database and directory structures to make proportional modules sizes. -------------------------------------------------------------------------------- /docs/images/project_gen.xml: -------------------------------------------------------------------------------- 1 | 7Vxdd6I4GP41Xk4PX4Jeau3MXOye7dnuOTNztSeVVOkgYUNsdX79JpIA+UDxA9Q6vWglhIDP+7zfoT33frH6gkE6/xOFMO45VrjquZOe49iea9E/bGSdj/hDPjDDUcgnlQNP0S/IB8W0ZRTCTJpIEIpJlMqDU5QkcEqkMYAxepenvaBYvmsKZlAbeJqCWB/9FoVkno8O+lY5/hVGs7m4s23xMwsgJvOBbA5C9F4Zch967j1GiOSfFqt7GDPwBC75dZ9rzhYPhmFCmlzwNnpNp4vn8K9fyffRP+t/v/49WX0KgnyZNxAv+TfmT0vWAoIZRsuUT4OYwJUJePAsplv6g9nF16U8gWgBCV7TKWKhfn4FZ4hY4L2E23P4lHkFakfIAHARz4qVSxToBw7EHqDYGib3KHmJZj3Hj+nNx8+YfpqxTxpWFKokhGxxi55+n0cEPqVgys6+UwWhY3OyoA8zsYurDZhWsdsitlpE+xqCAqsqgG5b+Dk6gBpQMKRqxg8RJnM0QwmIH8rRsQxlBbZXSMiaWwqwJIgOlSv8gVAqwctudAi49GnREk/hNp7USAHDGJDoTb6tCWV+6SOK6AOV+uBZkkLYjiIoAvAMEn5VKasRxmBdmZayCdn+9ylFn69YEqH4jkfolm5wNp6DDn2BCcSAIHxpWsXxGeiGyTeoVXtmaXAjasXd/IblbarfcdIY6l5ijlBGg5XdboJSk8joZwSjn/AexZT+7iRBCZPVSxTHyhCIo1lCD6cUbEjHx4zoEY1XRvzEIgrDjaBNaiILvzX/48qGxTP4I4NDt1vzR44uKgwBgdmtCWYgh1rD4E4XjWewaaoHOp1oXE00jxi9skhecghXEngVLnWHj/Baw9PT8KQ52ZKidC0QqumAAdFug9n+RXhdiilef2fXU5Xlhz/4cpuDyUo6WkumpEVvLTR4p7vOqXkud+00SHRhEo5YyYBZ8RhkWTSVZVUnA7tlGeyG1jVDu0NnxNix+Uogu/uhooo5hbR0RVunLyu+7TdLe06VmTh6eH1tpjO4MNOph8hXhqjrXhaibr17t0SNyvocXRPCCmdtuxnErWXZ7mX4+/b99rChc3HP6reFKt1k+HXy0qLjmmvt+7pq11Iy/G5dtasn9L9JcTgphicihX9eUuilBOoSs+Xid0WO1dZkGff1uo/fZUnOa6DBx/cdDyvhyEgNTDUyU01H4Hd6F9igndYSVrmYtiaWnULRxO7HcZRmdepW0W+QpfmGgZdoxRTwCOVzaxrcFZgcA0ytVVV1S3jRMK1FUKHpWaeo6bnVRaNm7rUY0qdOQbSbqGhZZ+M+tFpkW0Ukj6mGnsePN0HVXVAcl3EVO1hXDh4hjugXYX64DLy+Vw9+yPHaiUM0Yat3p1+9ZtlXTfLVTWVP4ZZakGtc2DtrXGg3sYZbGXlg2bdksuvbEpMt296bya0zsnEd/5yMNJfj9iVkzY6zrgjZxNE0I+QhdJSouJWGF0O5sxpBR7ZegwM5pzRJ1BJx25zTq5qjZ5rhgk2jfQJTSjiYTBmqfDvWB8uP94uiDDtW3E5DUUMtQ+xYufVSRpFdcllZd7Zf+ekHjia7Tre0eA3M+3lKG2qhz9RrCbYY0tPnqXrL9Sn3GXkXqwnbO2pjeYf5oNY6gYLSVwxdUZqUSXnmFqunV9skYK8GVyVuMbxW0W3rWt8MoAF3W33VgVl83fRVA91+fEhx+DVx37FJgZKJdp1D+rqV+oapnWHEQyyijzCNIBAnyrlLmDv3LSkuYKjbKmOboS1b5TcpGX0A5RDbiXbaqtNo0b4vKHnKLgOx0bC2Aa3ySJ7fzgtNvnMbXBFx0YVyxVVfZrtErgSmTm4eTobRm8QZ/78lezF4zJL5TzwvH7HcME/Ni/NlEJqvk6UgEWPjZRSHPeaZfLBg1j3/TUfGMJnOFwD/7BVvWUA2s3j3Il+MfsnqepXhzeOeIvwVJYcYvpA2fYy6AcIe6iUe29Qpay35DXTDcfFvQppTtobuuj0kdXd9LUg6avJ7biib7H7d8ZpECLL5Bli5nduv9B22t79a92V1r6J01FBQ2k+2mnM3bSk4nlkPO8pBAv8muFKTpnfVgZe5EhxIFXXD7smoQg/Lf2GSTy//EYz78D8= -------------------------------------------------------------------------------- /uberpoet/loccalc.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Uber Technologies, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging 16 | import tempfile 17 | from math import ceil 18 | from os.path import join 19 | 20 | from .commandlineutil import count_loc 21 | from .filegen import Language 22 | from .memoize import memoized 23 | 24 | 25 | class LOCCalculator(object): 26 | 27 | @memoized 28 | def calculate_loc(self, text, language): 29 | # actual code = lines of code, minus whitespace 30 | # calculated using cloc 31 | if language == Language.SWIFT: 32 | extension = '.swift' 33 | elif language == Language.OBJC: 34 | extension = '.m' 35 | else: 36 | raise ValueError("Unknown language: {}".format(language)) 37 | 38 | tmp_file_path = join(tempfile.gettempdir(), 'ub_mock_gen_example_file{}'.format(extension)) 39 | with open(tmp_file_path, 'w') as f: 40 | f.write(text) 41 | loc = count_loc(tmp_file_path, language) 42 | 43 | if loc == -1: 44 | logging.warning("Using fallback loc calc method due to cloc not being installed.") 45 | if language == Language.SWIFT: 46 | # fallback if cloc is not installed 47 | # this fallback is based on running cloc on the file made by `self.swift_gen.gen_file(3, 3)` 48 | # and saving the result of cloc(file_result.text) / file_result.text_line_count to here: 49 | fallback_code_multiplier = 0.811537333 50 | elif language == Language.OBJC: 51 | # fallback if cloc is not installed 52 | # this fallback is based on running cloc on the file made by `self.objc_source_gen.gen_file(3, 3)` 53 | # and saving the result of cloc(file_result.text) / file_result.text_line_count to here: 54 | fallback_code_multiplier = 0.772727272 55 | else: 56 | raise ValueError('No fallback multiplier calculated for language: {}'.format(language)) 57 | 58 | loc = int(ceil(len(text.split('\n')) * fallback_code_multiplier)) 59 | 60 | return loc 61 | -------------------------------------------------------------------------------- /uberpoet/resources/mockappdelegate: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | {0} 16 | 17 | import UIKit 18 | {1} 19 | 20 | @UIApplicationMain 21 | class AppDelegate: UIResponder, UIApplicationDelegate {{ 22 | 23 | var window: UIWindow? 24 | 25 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {{ 26 | // Override point for customization after application launch. 27 | {2} 28 | return true 29 | }} 30 | 31 | func applicationWillResignActive(_ application: UIApplication) {{ 32 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 33 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 34 | }} 35 | 36 | func applicationDidEnterBackground(_ application: UIApplication) {{ 37 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 38 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 39 | }} 40 | 41 | func applicationWillEnterForeground(_ application: UIApplication) {{ 42 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 43 | }} 44 | 45 | func applicationDidBecomeActive(_ application: UIApplication) {{ 46 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 47 | }} 48 | 49 | func applicationWillTerminate(_ application: UIApplication) {{ 50 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 51 | }} 52 | }} 53 | -------------------------------------------------------------------------------- /uberpoet/util.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Uber Technologies, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import distutils.spawn 16 | import math 17 | import os 18 | import subprocess 19 | 20 | 21 | class SeedContainer(object): 22 | """Holds the seed number variable""" 23 | seed = 0 24 | 25 | 26 | def bool_xor(a, b): 27 | """Python's ^ operator is a bitwise xor, so we need to make a boolean equivalent function.""" 28 | return (a and not b) or (not a and b) 29 | 30 | 31 | def seed(): 32 | """Gives you a unique number for codegen ids""" 33 | SeedContainer.seed += 1 34 | return SeedContainer.seed 35 | 36 | 37 | def first_in_dict(d): 38 | """Grabs the value returned by the first value in d.keys()""" 39 | if len(d) > 0: 40 | k = d.keys()[0] 41 | return d[k] 42 | return None 43 | 44 | 45 | def first_key(dictionary_var): 46 | """dictionary_var.keys()[0]""" 47 | return dictionary_var.keys()[0] 48 | 49 | 50 | def makedir(path): 51 | """Does a mkdir -p `path` if it doesn't exist""" 52 | if not os.path.exists(path): 53 | os.makedirs(path) 54 | 55 | 56 | def merge_lists(two_d_list): 57 | """Merges a 2d array into a 1d array. Ex: [[1,2],[3,4]] becomes [1,2,3,4]""" 58 | # I know this is a fold / reduce, but I got an error when I tried 59 | # the reduce function? 60 | return [i for li in two_d_list for i in li] 61 | 62 | 63 | def percentage_split(list_to_split, percentages): 64 | """Splits an array based on the percentages provided.""" 65 | result = [] 66 | previous_index = 0 67 | for percentage in percentages: 68 | next_index = int(previous_index + math.ceil((len(list_to_split) * percentage))) 69 | result.append(list_to_split[previous_index:next_index]) 70 | previous_index = next_index 71 | 72 | return result 73 | 74 | 75 | def sudo_enabled(): 76 | """Tells you if the current 'shell' has sudo permission.""" 77 | try: 78 | subprocess.check_call(['sudo', '-n', 'true']) 79 | return True 80 | except subprocess.CalledProcessError: 81 | return False 82 | 83 | 84 | def check_dependent_commands(command_list): 85 | """ 86 | Checks if the commands are accessible by the process. 87 | Returns a list of misssing commands. Empty if all commands are available. 88 | """ 89 | # noinspection PyUnresolvedReferences 90 | return [command for command in command_list if not distutils.spawn.find_executable(command)] 91 | 92 | 93 | def pad_list(l, size, value=0): 94 | """Pads the right side of the list `l` to `size` if len(l) is less than size with `value`.""" 95 | if len(l) >= size: 96 | return l 97 | 98 | return l + ([value] * (size - len(l))) 99 | 100 | 101 | def grab_mac_marketing_name(): 102 | """ 103 | Returns a string telling you you the macOS marketing name of the device your running on. 104 | Ex: "MacBook Pro (15-inch, 2018)" 105 | """ 106 | script_path = os.path.join(os.path.dirname(__file__), "resources", "get_market_name.sh") 107 | return subprocess.check_output([script_path]) 108 | -------------------------------------------------------------------------------- /uberpoet/resources/mockbazelworkspace: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Uber Technologies, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # This code was @generated by Uber Poet, a mock application generator. 16 | # Check it out at https://github.com/uber/uber-poet 17 | 18 | # Description: 19 | # Repository-wide definitions for the seller/register bazel build system. 20 | # 21 | # WORKSPACE defines the root of a bazel environment, and contains infrastructural rules that 22 | # further define the workspace environment. Any "repositories" need to be defined here, so here 23 | # we import all the other bazel projects we consume (mostly skylark libraries or custom bazel 24 | # rules), the core statements that do the fetching of maven artifacts when required, etc. 25 | # 26 | # Rules invoked from the WORKSPACE will only ever be run during the configuration of this bazel 27 | # workspace, and this configuration is cached until invalidated (e.g. bumping a version number, 28 | # or adding a dep, or changing the set of bazel rules we consume). This is the only place where 29 | # bazel permits dynamism based on environment, as it locks down the sandboxes in which each 30 | # build tool executes during the normal build phase. 31 | # 32 | # Specific configuration values have been extracted to make this file a bit more terese, primarily 33 | # versions.bzl which contains globals and environment-wide version numbers shared by everyone, 34 | # and third_party/maven_artifacts.bzl which define any maven artifacts and thier versions that 35 | # will be available to bazel targets (see third_party/MAVEN.md for more details). 36 | # 37 | # Custom rule definitions not imported externally are defined in //third_party/tools/build_defs, 38 | # and other useful bazel tooling will be found in //tools subpackages. 39 | 40 | # Load global values (mostly version numbers) used in the WORKSPACE. 41 | # load( 42 | # "//:workspace_versions.bzl", 43 | # "RULES_APPLE_VERSION", 44 | # ) 45 | 46 | workspace(name = "App") 47 | 48 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 49 | load("//tools/rules:repository_rules.bzl", "github_repo") 50 | 51 | github_repo( 52 | name = "build_bazel_rules_ios", 53 | project = "bazel-ios", 54 | ref = "b439882984e3e756f8a54a589b6aa3e7071bb502", 55 | repo = "rules_ios", 56 | sha256 = "5615afd1e20cb33cb26a57c6dc3a0cc586d4571480ec0b24377c9b781337d6de", 57 | ) 58 | 59 | load( 60 | "@build_bazel_rules_ios//rules:repositories.bzl", 61 | "rules_ios_dependencies", 62 | ) 63 | 64 | rules_ios_dependencies() 65 | 66 | load( 67 | "@bazel_skylib//:workspace.bzl", 68 | "bazel_skylib_workspace", 69 | ) 70 | 71 | bazel_skylib_workspace() 72 | 73 | load( 74 | "@build_bazel_rules_apple//apple:repositories.bzl", 75 | "apple_rules_dependencies", 76 | ) 77 | 78 | apple_rules_dependencies() 79 | 80 | load( 81 | "@build_bazel_rules_swift//swift:repositories.bzl", 82 | "swift_rules_dependencies", 83 | ) 84 | 85 | swift_rules_dependencies() 86 | 87 | load( 88 | "@build_bazel_apple_support//lib:repositories.bzl", 89 | "apple_support_dependencies", 90 | ) 91 | 92 | apple_support_dependencies() 93 | 94 | load( 95 | "@com_google_protobuf//:protobuf_deps.bzl", 96 | "protobuf_deps", 97 | ) 98 | 99 | protobuf_deps() 100 | -------------------------------------------------------------------------------- /tests/test_filegen.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Uber Technologies, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import unittest 16 | 17 | from uberpoet.filegen import (FileResult, FuncType, Language, get_func_call_template, get_import_func_calls, 18 | objc_to_objc_func_call_template, objc_to_swift_func_call_template, 19 | swift_to_objc_friendly_func_call_template, swift_to_objc_func_call_template, 20 | swift_to_swift_func_call_template, swift_to_swift_objc_friendly_func_call_template) 21 | 22 | 23 | class TestFileGen(unittest.TestCase): 24 | 25 | def test_func_call_template(self): 26 | # From Swift to ObjC all method calls can be invoked. 27 | self.assertEqual(get_func_call_template(Language.SWIFT, Language.SWIFT, FuncType.SWIFT_ONLY), 28 | swift_to_swift_func_call_template) 29 | self.assertEqual(get_func_call_template(Language.SWIFT, Language.OBJC, FuncType.SWIFT_ONLY), 30 | swift_to_objc_func_call_template) 31 | self.assertEqual(get_func_call_template(Language.SWIFT, Language.SWIFT, FuncType.OBJC_FRIENDLY), 32 | swift_to_swift_objc_friendly_func_call_template) 33 | self.assertEqual(get_func_call_template(Language.SWIFT, Language.OBJC, FuncType.OBJC_FRIENDLY), 34 | swift_to_objc_friendly_func_call_template) 35 | 36 | # From ObjC to Swift we cannot invoke methods that are only available to Swift. 37 | with self.assertRaises(ValueError): 38 | get_func_call_template(Language.OBJC, Language.SWIFT, FuncType.SWIFT_ONLY) 39 | get_func_call_template(Language.OBJC, Language.OBJC, FuncType.SWIFT_ONLY) 40 | 41 | # From ObjC to Swift with ObjC friendly methods 42 | self.assertEqual(get_func_call_template(Language.OBJC, Language.SWIFT, FuncType.OBJC_FRIENDLY), 43 | objc_to_swift_func_call_template) 44 | self.assertEqual(get_func_call_template(Language.OBJC, Language.OBJC, FuncType.OBJC_FRIENDLY), 45 | objc_to_objc_func_call_template) 46 | 47 | def test_import_func_calls_swift(self): 48 | expected_swift_func_calls = """MyClass0().complexCrap0(arg: 4, stuff: 2) 49 | MyClass0().complexCrap1(arg: 4, stuff: 2) 50 | MyClass0().complexCrap2(arg: 4, stuff: 2) 51 | MyClass0().complexStuff0(arg: "4")""" 52 | swift_import = self._create_import('MockLib0', Language.SWIFT) 53 | self.assertEqual(get_import_func_calls(Language.SWIFT, [swift_import]), expected_swift_func_calls) 54 | 55 | def test_import_func_calls_objc(self): 56 | expected_objc_func_calls = """[[[MyClass_0 alloc] init] complexCrap0:4 stuff:@"2"]; 57 | [[[MyClass_0 alloc] init] complexCrap1:4 stuff:@"2"]; 58 | [[[MyClass_0 alloc] init] complexCrap2:4 stuff:@"2"];""" 59 | objc_import = self._create_import('MockLib0', Language.OBJC) 60 | self.assertEqual(get_import_func_calls(Language.OBJC, [objc_import]), expected_objc_func_calls) 61 | 62 | def test_import_func_calls_for_str(self): 63 | self.assertEqual(get_import_func_calls(Language.SWIFT, ['File.h']), '') 64 | 65 | def _create_import(self, name, language): 66 | file_name = 'File0.swift' if language == Language.SWIFT else 'File0.m' 67 | if language == Language.SWIFT: 68 | class_functions = {FuncType.SWIFT_ONLY: [0, 1, 2], FuncType.OBJC_FRIENDLY: [0]} 69 | elif language == Language.OBJC: 70 | class_functions = {FuncType.OBJC_FRIENDLY: [0, 1, 2]} 71 | file_result = FileResult('body', [], {0: class_functions}) 72 | return {name: {'files': {file_name: file_result}, 'loc': 150, 'language': language}} 73 | -------------------------------------------------------------------------------- /uberpoet/moduletree.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Uber Technologies, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from __future__ import absolute_import 16 | 17 | import random 18 | 19 | from toposort import toposort_flatten 20 | 21 | from .util import merge_lists 22 | 23 | 24 | class ModuleGenType(object): 25 | flat = 'flat' 26 | bs_flat = 'bs_flat' 27 | layered = 'layered' 28 | bs_layered = 'bs_layered' 29 | dot = 'dot' 30 | 31 | @staticmethod 32 | def enum_list(): 33 | return [ 34 | ModuleGenType.flat, 35 | ModuleGenType.bs_flat, 36 | ModuleGenType.layered, 37 | ModuleGenType.bs_layered, 38 | ModuleGenType.dot, 39 | ] 40 | 41 | 42 | class ModuleNode(object): 43 | """Represents a module in a dependency graph""" 44 | 45 | APP = 'APP' 46 | LIBRARY = 'LIBRARY' 47 | 48 | def __init__(self, name, node_type, deps=None): 49 | super(ModuleNode, self).__init__() 50 | if deps is None: 51 | deps = [] 52 | self.name = name 53 | self.node_type = node_type # app or library 54 | self.deps = deps 55 | # How many code units the module represents. Bigger modules would 56 | # have more code units than smaller modules, with 1 being the 'standard' size. 57 | self.code_units = 1 58 | self.extra_info = None # useful for file indexes and such 59 | 60 | def __hash__(self): 61 | return hash((self.name, self.node_type)) 62 | 63 | def __eq__(self, other): 64 | return (self.name, self.node_type) == (other.name, other.node_type) 65 | 66 | def __repr__(self): 67 | return "ModuleNode('{}','{}')".format(self.name, self.node_type) 68 | 69 | def __str__(self): 70 | extra = True if self.extra_info else False 71 | return "<{} : {} deps: {} has_info: {}>".format(self.name, self.node_type, len(self.deps), extra) 72 | 73 | @staticmethod 74 | def gen_layered_graph(layer_count, nodes_per_layer, deps_per_node=5): 75 | """Generates a module dependency graph that has `layer_count` layers, 76 | with each module only depending on a random selection of the modules 77 | below it""" 78 | 79 | def node(f_layer, f_node): 80 | return ModuleNode('MockLib{}_{}'.format(f_layer, f_node), ModuleNode.LIBRARY) 81 | 82 | layers = [[node(l, n) for n in xrange(nodes_per_layer)] for l in xrange(layer_count)] 83 | app_node = ModuleNode('App', ModuleNode.APP, layers[0]) 84 | 85 | node_graph = {} 86 | 87 | for i, layer in enumerate(layers): 88 | lower_layers = layers[(i + 1):] if i != len(layers) - 1 else [] 89 | lower_merged = merge_lists(lower_layers) 90 | for node in layer: 91 | if deps_per_node < len(lower_merged): 92 | node.deps = random.sample(lower_merged, deps_per_node) 93 | else: 94 | node.deps = lower_merged 95 | 96 | node_graph[node] = set(node.deps) 97 | 98 | return app_node, (toposort_flatten(node_graph) + [app_node]) 99 | 100 | @staticmethod 101 | def gen_flat_graph(module_count): 102 | """Generates a module dependency graph that depends on `module_count` 103 | libraries that don't depend on anything""" 104 | libraries = [ModuleNode('MockLib{}'.format(i), ModuleNode.LIBRARY) for i in xrange(module_count)] 105 | app_node = ModuleNode('App', ModuleNode.APP, libraries) 106 | 107 | return app_node, (libraries + [app_node]) 108 | 109 | @staticmethod 110 | def gen_flat_big_small_graph(big_mod_count, small_mod_count): 111 | big_libs = [ModuleNode('BigMockLib{}'.format(i), ModuleNode.LIBRARY) for i in xrange(big_mod_count)] 112 | small_libs = [ModuleNode('SmallMockLib{}'.format(i), ModuleNode.LIBRARY) for i in xrange(small_mod_count)] 113 | app_node = ModuleNode('App', ModuleNode.APP, big_libs + small_libs) 114 | 115 | for l in big_libs: 116 | l.code_units = 20 117 | 118 | return app_node, (big_libs + small_libs + [app_node]) 119 | 120 | @staticmethod 121 | def gen_layered_big_small_graph(big_mod_count, small_mod_count): 122 | big_libs = [ModuleNode('AppMockLib{}'.format(i), ModuleNode.LIBRARY) for i in xrange(big_mod_count)] 123 | app_node = ModuleNode('App', ModuleNode.APP, big_libs) 124 | 125 | layer_count = 3 126 | layer_mod_count = small_mod_count / layer_count 127 | deps_per_layer = layer_count / 2 if layer_count >= 2 else 1 128 | 129 | layer_app_node, layer_nodes = ModuleNode.gen_layered_graph(layer_count, layer_mod_count, deps_per_layer) 130 | layer_nodes = [layer_item for layer_item in layer_nodes if layer_item != layer_app_node] 131 | 132 | for l in big_libs: 133 | l.code_units = 20 134 | l.deps = layer_app_node.deps 135 | 136 | node_graph = {n: set(n.deps) for n in big_libs + layer_nodes} 137 | return app_node, (toposort_flatten(node_graph) + [app_node]) 138 | -------------------------------------------------------------------------------- /uberpoet/cpulogger.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Uber Technologies, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging 16 | import os 17 | import subprocess 18 | import sys 19 | from tempfile import TemporaryFile 20 | 21 | 22 | class CPULog(object): 23 | """An object representation of a CPU state log""" 24 | EPOCH_MULT = 1000000 25 | 26 | def __init__(self, line=None): 27 | self.epoch = None 28 | self.sys = None 29 | self.user = None 30 | self.idle = None 31 | if line: 32 | self.parse_line(line) 33 | 34 | def parse_line(self, line): 35 | """Parses a top CPU log string into data for this object""" 36 | 37 | def percent_to_num(raw): 38 | return float(raw[:-1]) * 0.01 39 | 40 | line = str(line) 41 | items = line.split(" ") 42 | # filter out null chars that sometimes showup 43 | epoch_str = items[0].translate(None, '\x00') 44 | 45 | self.epoch = int(epoch_str) 46 | self.user = percent_to_num(items[3]) 47 | self.sys = percent_to_num(items[5]) 48 | self.idle = percent_to_num(items[7]) 49 | 50 | def chrome_trace(self): 51 | """Returns the chrome trace json representation of the the object""" 52 | return { 53 | "name": "cpu", 54 | "ph": "C", 55 | "pid": 1, 56 | "ts": self.chrome_epoch, 57 | "args": { 58 | "user": self.user, 59 | "sys": self.sys, 60 | "idle": self.idle 61 | } 62 | } 63 | 64 | @property 65 | def chrome_epoch(self): 66 | """Turns the internal epoch represenation into the time unit that chrome traces expect""" 67 | return self.epoch * CPULog.EPOCH_MULT 68 | 69 | def chrome_epoch_in_range(self, min_ts, max_ts): 70 | """Returns true if this object is within the specified time range""" 71 | return min_ts <= self.chrome_epoch <= max_ts 72 | 73 | @staticmethod 74 | def find_timestamp_range(traces): 75 | """ 76 | Finds the minimum and maximum times of items inside a chrome trace list, 77 | so the CPU log won't add CPU items outside of it's range. 78 | """ 79 | min_ts = sys.maxsize 80 | max_ts = -1 81 | for trace in traces: 82 | ts = trace['ts'] 83 | if ts < min_ts: 84 | min_ts = ts 85 | elif ts > max_ts: 86 | max_ts = ts 87 | return min_ts, max_ts 88 | 89 | @staticmethod 90 | def apply_log_to_trace(log_list, traces): 91 | """Adds CPU items to a chrome trace""" 92 | min_ts, max_ts = CPULog.find_timestamp_range(traces) 93 | traces_in_range = [i.chrome_trace() for i in log_list if i.chrome_epoch_in_range(min_ts, max_ts)] 94 | return traces + traces_in_range 95 | 96 | 97 | # TODO Use psutil instead of top bash script to fix app killing issues? 98 | class CPULogger(object): 99 | """ 100 | This class uses the top command under the hood to continiously log system cpu 101 | utilization on the current system. 102 | """ 103 | 104 | def __init__(self): 105 | self.process = None 106 | self.output = TemporaryFile() 107 | 108 | def __del__(self): 109 | self.output.close() 110 | 111 | def start(self): 112 | """Starts the CPU logger""" 113 | self.stop() 114 | logging.warning('You will probably have to call `sudo killall top` to' 115 | ' kill the CPU monitor after this python script finishes execution.') 116 | script_path = os.path.join(os.path.dirname(__file__), "resources", "cpu_log.sh") 117 | self.process = subprocess.Popen([script_path], stdout=self.output) 118 | 119 | def stop(self): 120 | """ 121 | Should stop the CPU logger. 122 | 123 | MAJOR 124 | TODO doesn't do anything, either because top runs as root and thus you need 125 | to be root to kill it or some process group thing we are not doing properly 126 | self.process.kill() doesnt work either. 127 | """ 128 | if self.process: 129 | self.process.terminate() 130 | self.process = None 131 | 132 | def kill(self): 133 | """ 134 | Attempts to use sudo to kill the dangling CPU monitor. Currently doesn't seem to work, 135 | you need to kill the top process outside of the python process. 136 | """ 137 | self.stop() 138 | command = ['sudo', 'killall', 'top'] 139 | logging.warning('Killing dangling CPU monitor with sudo. Command: `%s`', ' '.join(command)) 140 | try: 141 | subprocess.check_call(command) 142 | except subprocess.CalledProcessError: 143 | logging.info("Error killing top command") 144 | 145 | def process_log(self): 146 | """Stops the CPU logger and converts the internal CPU log strings into a list of CPULog objects.""" 147 | self.stop() 148 | self.output.seek(0) 149 | out = [CPULog(line) for line in self.output] 150 | self.output.close() 151 | self.output = TemporaryFile() 152 | return out 153 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | Uber welcomes contributions of all kinds and sizes. This includes everything from from simple bug reports to large features. 4 | 5 | Before we can accept your contributions, we kindly ask you to sign our [Contributor License Agreement](https://cla-assistant.io/uber/uber-poet). 6 | 7 | ## Workflow 8 | 9 | We love GitHub issues! 10 | 11 | For small feature requests, an issue first proposing it for discussion or demo implementation in a PR suffice. 12 | 13 | For big features, please open an issue so that we can agree on the direction, and hopefully avoid investing a lot of time on a feature that might need reworking. 14 | 15 | Small pull requests for things like typos, bug fixes, etc are always welcome. 16 | 17 | ## DOs and DON'Ts 18 | 19 | * DO read this document to understand how Uber Poet is built and how we develop. 20 | * DO write your change to conform to our formatting, lint, import sorting, etc tools before uploading your pull request. 21 | * DO add types if possible with new and old code. Types help with refactoring and are unit tests you don't have to write. 22 | * DO include tests when adding new features. When fixing bugs, start with adding a test that highlights how the current behavior is broken. 23 | * DO keep the discussions focused. When a new or related topic comes up it's often better to create new issue than to side track the discussion. 24 | * DON'T not copy code from websites with licenses that are incompatible with Apache 2.0. This means you cannot copy from StackOverflow, because it's Creative Commons BY-SA license which is not compatible. 25 | * DON'T submit PRs that alter licensing related files or headers. If you believe there's a problem with them, file an issue and we'll be happy to discuss it. 26 | 27 | ## Basic Getting Started 28 | 29 | * On a macOS 10.13+ machine, install Xcode : `xcode-select --install` 30 | * Install [pipenv](https://pipenv.readthedocs.io/en/latest/) 31 | * Clone the repo: `git clone https://github.com/uber/uber-patcher.git` 32 | * Install python dependencies: `pipenv install --dev` 33 | * Test if your pipenv setup is working by running unit tests: `pipenv run pytest` 34 | * You can also run longer running integration tests with `INTEGRATION=1 pipenv run pytest` 35 | * Edit code with your favorite text editor or IDE! 36 | 37 | ## How we Develop 38 | 39 | * We used [Visual Studio Code](https://code.visualstudio.com) at first, but then moved to [pycharm](https://www.jetbrains.com/pycharm/) because of it's code inspector, richer refactoring tools and less configuration required to do basic things like testing. You can use any code editor you would like, but please use PyCharm's [Inspect Code](https://www.jetbrains.com/help/pycharm/running-inspections.html) before you push. 40 | * It's a goal to eventually move this code base to python 3, probably when python 2.7 won't be maintained anymore after 2020. 41 | 42 | Tools used for managing the code base: 43 | * pytest for running tests, basic unittest for writing tests. Run tests with your IDE or `pipenv run pytest`. 44 | * yapf for formatting 45 | * isort for sorting imports 46 | * flake8 & pylint for linting 47 | * PyCharm's "[Inspect Code](https://www.jetbrains.com/help/pycharm/running-inspections.html)" tool (not enforced by ci since it's difficult to use via command line) 48 | * pipenv to manage application dependencies 49 | * Tool configuration is kept in the `setup.cfg` file. Please use that as you develop. 50 | 51 | I would suggest running the dev tool versions that come with the repo, to avoid issues with github actions CI not passing. Look at the [github actions yaml](../.github/workflows/python-app.yml) to see what commands it executes. The versions they execute although are not ones that would actually update your code to automatically fix them, so a general pre-commit workflow would be: 52 | 53 | ```bash 54 | pipenv install --dev #only need to run this once 55 | INTEGRATION=1 pipenv run pytest --cov=uberpoet --cov-report xml:cov.xml --cov-report term-missing #fix failing tests 56 | pipenv run yapf -ri uberpoet/ test/ *py # -r = recursive , -i = in-place, yapf is an autoformatter 57 | pipenv run isort # isort fixes in place by default, isort = import sort 58 | pipenv run flake8 # manually fix any issues the flake8 linter brings up 59 | ``` 60 | 61 | Make sure to check the [.travis.yml](../.travis.yml) file to see what is actually run in case this file is out of date. 62 | 63 | ## Basic App Architecture 64 | 65 | If you were to summarize Uber Poet as some python pseudocode: 66 | 67 | ```python 68 | def make_project(config): 69 | abstract_dependency_graph = config.graph_generation_function(config.project_generation_options) 70 | swift_file_maker = SwiftFileGenerator() 71 | project_gen = BuckProjectGenerator(swift_file_maker) 72 | project_gen.generate_project_from(abstract_dependency_graph) 73 | project_gen.write_to_folder(config.output_path) 74 | 75 | def multisuite(config): 76 | original_state = save_xcode_state() 77 | 78 | for xcode_config in config.xcode_configs: 79 | set_xcode_state(xcode_config) 80 | 81 | for gen_func in config.graph_generation_functions: 82 | proj_config = ProjectConfig(gen_func, join(config.output_path, gen_func.name) 83 | make_project(proj_config) 84 | 85 | trace = TimeAndCPUTracer().start() 86 | build_project(proj_config) 87 | trace.stop() 88 | 89 | trace.append_result_to_csv(config, proj_config) 90 | 91 | set_xcode_state(original_state) 92 | ``` 93 | 94 | ![mock application generation code flow](images/project_gen.png) 95 | 96 | Generating a mock app consists of 3 parts: 97 | 98 | * Generating an abstract module dependency graph that represents the mock app. (`ModuleNode` in `moduletree.py`) 99 | * Feeding this graph into a build description generator (ex: `BuckProjectGenerator` in `projectgen.py`), which creates project config files. 100 | * And the build description generator using a file generator (ex `SwiftFileGenerator` in `filegen.py`) creating mock code files. 101 | * And then writing all these files into a tree of folders 102 | 103 | Most variation & configuration resides in the graph objects that the graph generation functions create. So the if command line says there are 50 modules or 100 modules with a [`bs_layered`](docs/layer_types.md) graph type, that will show up in the generated abstract graph. 104 | 105 | `GenProjCommandLine` from `genproj.py` is the UI that the user's configuration for generating a mock app is passed into the above process. 106 | 107 | `CommandLineMultisuite` from `multisuite.py`, does the same thing as `genproj.py`, but with a list of project generators, and builds these projects and records the build times. It also gives you configuration options in how building is done. Multisuite also uses code in `statementmanagement.py` & `cpulogger.py` to help it manage Xcode build configuration and track CPU usage. 108 | 109 | `dotreader.py` is used to generate dependency graphs from dot files. 110 | -------------------------------------------------------------------------------- /uberpoet/statemanagement.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Uber Technologies, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from __future__ import absolute_import 16 | 17 | import getpass 18 | import logging 19 | import os 20 | import shutil 21 | import subprocess 22 | import sys 23 | from os.path import join 24 | 25 | from .util import pad_list 26 | 27 | 28 | class SettingsState(object): 29 | 30 | def __init__(self, git_root): 31 | self.git_root = git_root 32 | self.have_backed_up = False 33 | self.local_path = join(self.git_root, '.buckconfig.local') 34 | self.backup_path = join(self.git_root, '.buckconfig.local.bak') 35 | self.select_path = None 36 | 37 | def save_buckconfig_local(self): 38 | if os.path.exists(self.backup_path): 39 | os.remove(self.backup_path) 40 | if os.path.exists(self.local_path): 41 | logging.info('Backing up .buckconfig.local to .buckconfig.local.bak') 42 | shutil.copy2(self.local_path, self.backup_path) 43 | self.have_backed_up = True 44 | else: 45 | logging.info('No .buckconfig.local to back up, skipping') 46 | 47 | def restore_buckconfig_local(self): 48 | if self.have_backed_up: 49 | logging.info('Restoring .buckconfig.local') 50 | os.remove(self.local_path) 51 | shutil.copy2(self.backup_path, self.local_path) 52 | os.remove(self.backup_path) 53 | self.have_backed_up = False 54 | 55 | def save_xcode_select(self): 56 | self.select_path = subprocess.check_output(['xcode-select', '-p']).rstrip() 57 | 58 | def restore_xcode_select(self): 59 | if self.select_path: 60 | logging.info('Restoring xcode-select path to %s', self.select_path) 61 | subprocess.check_call(['sudo', 'xcode-select', '-s', self.select_path]) 62 | 63 | 64 | class XcodeManager(object): 65 | 66 | @staticmethod 67 | def get_xcode_dirs(containing_dir='/Applications'): 68 | items = os.listdir(containing_dir) 69 | return [join(containing_dir, d) for d in items if 'xcode' in d.lower() and d.endswith('app')] 70 | 71 | @staticmethod 72 | def get_current_xcode_version(): 73 | version_out = subprocess.check_output(['xcodebuild', '-version']).splitlines() 74 | version_num = version_out[0].split(' ')[1] 75 | build_id = version_out[1].split(' ')[2] 76 | return version_num, build_id 77 | 78 | @staticmethod 79 | def switch_xcode_version(xcode_path): 80 | subprocess.check_call(['sudo', 'xcode-select', '-s', xcode_path]) 81 | 82 | def xcode_version_of_path(self, path): 83 | try: 84 | self.switch_xcode_version(path) 85 | except subprocess.CalledProcessError: 86 | return None, None 87 | return self.get_current_xcode_version() 88 | 89 | def discover_xcode_versions(self): 90 | settings = SettingsState('/') 91 | settings.save_xcode_select() 92 | 93 | candidates = self.get_xcode_dirs() 94 | out = {} 95 | for path in candidates: 96 | version, build = self.xcode_version_of_path(path) 97 | if version: 98 | out[(version, build)] = path 99 | 100 | settings.restore_xcode_select() 101 | 102 | out = XcodeVersion.choose_latest_major_versions(out) 103 | 104 | return out 105 | 106 | @staticmethod 107 | def _get_global_module_cache_dir(): 108 | try: 109 | username = getpass.getuser() 110 | except Exception as e: 111 | sys.exit(str(e)) 112 | 113 | cache_dir = subprocess.check_output(['getconf', 'DARWIN_USER_CACHE_DIR']).rstrip() 114 | user_dir = 'org.llvm.clang.{}'.format(username) 115 | return os.path.join(cache_dir, user_dir, 'ModuleCache') 116 | 117 | def clean_caches(self): 118 | logging.info('Cleaning Xcode caches...') 119 | 120 | directories_to_delete = ( 121 | '~/Library/Caches/com.apple.dt.Xcode', 122 | '~/Library/Developer/Xcode/DerivedData', 123 | self._get_global_module_cache_dir(), 124 | ) 125 | 126 | for directory in directories_to_delete: 127 | full_path = os.path.expanduser(directory) 128 | logging.info('Removing %s', full_path) 129 | subprocess.check_call(['rm', '-fr', full_path]) 130 | 131 | 132 | class XcodeVersion(object): 133 | """Represents an xcode version that is comparable""" 134 | 135 | def __init__(self, raw_version, build): 136 | self.version = self.numeric_version(raw_version) 137 | self.raw_version = raw_version 138 | self.build = build 139 | 140 | @staticmethod 141 | def numeric_version(raw): 142 | return pad_list([int(x) for x in raw.split('.')], 3, 0) 143 | 144 | @property 145 | def major(self): 146 | return self.version[0] 147 | 148 | @property 149 | def raw(self): 150 | return self.raw_version, self.build 151 | 152 | @staticmethod 153 | def choose_latest_major_versions(raw_versions): 154 | """ 155 | This selects the latest version for each major version of xcode in a set of xcode paths. 156 | raw_versions is a {(version_str, build_str): xcode_path_str} dictionary. 157 | """ 158 | 159 | versions = {XcodeVersion(raw_version, build): path for (raw_version, build), path in raw_versions.iteritems()} 160 | 161 | major_seperated = {} 162 | for version, path in versions.iteritems(): 163 | subset = major_seperated.get(version.major, {}) 164 | subset[version] = path 165 | major_seperated[version.major] = subset 166 | 167 | out = {} 168 | for subset in major_seperated.values(): 169 | max_version = max(subset.keys()) 170 | out[max_version.raw] = subset[max_version] 171 | 172 | return out 173 | 174 | def __repr__(self): 175 | return "XcodeVersion('{}','{}')".format(self.raw_version, self.build) 176 | 177 | def __eq__(self, b): 178 | if not (tuple(self.version) == tuple(b.version)): 179 | return False 180 | if self.build == b.build: 181 | return True 182 | return False 183 | 184 | def __gt__(self, b): 185 | if self.version == b.version: 186 | if self.build > b.build: 187 | return True 188 | return False 189 | 190 | if tuple(self.version) > tuple(b.version): 191 | return True 192 | 193 | return False 194 | -------------------------------------------------------------------------------- /uberpoet/genproj.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Uber Technologies, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from __future__ import absolute_import 16 | 17 | import argparse 18 | import json 19 | import logging 20 | import sys 21 | import time 22 | from os.path import join 23 | 24 | from . import blazeprojectgen, commandlineutil, cpprojectgen 25 | from .moduletree import ModuleGenType 26 | 27 | 28 | class GenProjCommandLine(object): 29 | 30 | @staticmethod 31 | def make_args(args): 32 | """Parses command line arguments""" 33 | arg_desc = 'Generate a fake test project with many modules' 34 | 35 | parser = argparse.ArgumentParser(description=arg_desc) 36 | 37 | parser.add_argument('-o', '--output_directory', required=True, help='Where the mock project should be output.') 38 | parser.add_argument( 39 | '-pgt', 40 | '--project_generator_type', 41 | choices=['buck', 'bazel', 'cocoapods'], 42 | default='buck', 43 | required=False, 44 | help='The project generator type to use. Supported types are Buck, Bazel and CocoaPods. Default is `buck`') 45 | parser.add_argument( 46 | '-bmp', 47 | '--blaze_module_path', 48 | help='The root of the Buck or Bazel dependency path of the generated code. Only used if Buck or Bazel ' 49 | 'generator type is used.') 50 | parser.add_argument( 51 | '-gt', 52 | '--gen_type', 53 | required=True, 54 | choices=ModuleGenType.enum_list(), 55 | help='What kind of mock app generation you want. See layer_types.md for a description of graph types.') 56 | parser.add_argument( 57 | '-wmo', 58 | '--use_wmo', 59 | default=False, 60 | help='Whether or not to use whole module optimization when building swift modules.') 61 | parser.add_argument( 62 | '-udl', 63 | '--use_dynamic_linking', 64 | default=False, 65 | help='Whether or not to generate a project in which the modules are dynamically linked. By default all ' 66 | 'projects use static linking. This option is currently used only by the CocoaPods generator.') 67 | parser.add_argument( 68 | '--print_dependency_graph', 69 | default=False, 70 | help='If true, prints out the dependency edge list and exits instead of generating an application.') 71 | # CocoaPods specific options 72 | parser.add_argument( 73 | '--cocoapods_use_deterministic_uuids', 74 | default=True, 75 | help='Whether to use deterministic uuids within the CocoaPods generated project. Defaults to `true`.') 76 | parser.add_argument( 77 | '--cocoapods_generate_multiple_pod_projects', 78 | default=False, 79 | help='Whether to generate multiple pods projects. Defaults to `false`.') 80 | 81 | commandlineutil.AppGenerationConfig.add_app_gen_options(parser) 82 | args = parser.parse_args(args) 83 | commandlineutil.AppGenerationConfig.validate_app_gen_options(args) 84 | 85 | return args 86 | 87 | def main(self, args=None): 88 | if args is None: 89 | args = sys.argv[1:] 90 | 91 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(funcName)s: %(message)s') 92 | start = time.time() 93 | 94 | args = self.make_args(args) 95 | 96 | graph_config = commandlineutil.AppGenerationConfig() 97 | graph_config.pull_from_args(args) 98 | app_node, node_list = commandlineutil.gen_graph(args.gen_type, graph_config) 99 | 100 | if args.print_dependency_graph: 101 | print_nodes(node_list) 102 | exit(0) 103 | 104 | commandlineutil.del_old_output_dir(args.output_directory) 105 | gen = project_generator_for_arg(args) 106 | 107 | logging.info("Project Generator type: %s", args.project_generator_type) 108 | logging.info("Generation type: %s", args.gen_type) 109 | logging.info("Creating a {} module count mock app in {}".format(len(node_list), args.output_directory)) 110 | logging.info("Example command to generate Xcode workspace: $ {}".format(gen.example_command())) 111 | 112 | gen.gen_app(app_node, node_list, graph_config.swift_lines_of_code, graph_config.objc_lines_of_code, 113 | graph_config.loc_json_file_path) 114 | 115 | fin = time.time() 116 | logging.info("Done in %f s", fin - start) 117 | 118 | project_info = { 119 | "generator_type": args.project_generator_type, 120 | "graph_config": args.gen_type, 121 | "options": { 122 | "use_wmo": bool(args.use_wmo), 123 | "use_dynamic_linking": bool(args.use_dynamic_linking), 124 | "swift_lines_of_code": args.swift_lines_of_code, 125 | "objc_lines_of_code": args.objc_lines_of_code 126 | }, 127 | "time_to_generate": fin - start 128 | } 129 | with open(join(args.output_directory, "project_info.json"), "w") as project_info_json_file: 130 | json.dump(project_info, project_info_json_file) 131 | 132 | 133 | def print_nodes(node_list): 134 | edges = [(node.name, dep.name) for node in node_list for dep in node.deps] 135 | for edge in edges: 136 | print(edge[0], edge[1]) 137 | 138 | 139 | def project_generator_for_arg(args): 140 | if args.project_generator_type == 'buck' or args.project_generator_type == 'bazel': 141 | if not args.blaze_module_path: 142 | raise ValueError("Must supply --blaze_module_path when using the Buck or Bazel generators.") 143 | return blazeprojectgen.BlazeProjectGenerator( 144 | args.output_directory, args.blaze_module_path, use_wmo=args.use_wmo, flavor=args.project_generator_type) 145 | elif args.project_generator_type == 'cocoapods': 146 | return cpprojectgen.CocoaPodsProjectGenerator( 147 | args.output_directory, 148 | use_wmo=args.use_wmo, 149 | use_dynamic_linking=args.use_dynamic_linking, 150 | use_deterministic_uuids=args.cocoapods_use_deterministic_uuids, 151 | generate_multiple_pod_projects=args.cocoapods_generate_multiple_pod_projects) 152 | else: 153 | raise ValueError("Unknown project generator arg: " + str(args.project_generator_type)) 154 | 155 | 156 | def main(): 157 | GenProjCommandLine().main() 158 | 159 | 160 | if __name__ == '__main__': 161 | main() 162 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Uber Poet 2 | 3 | [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/2983/badge)](https://bestpractices.coreinfrastructure.org/projects/2983) 4 | [![Build Status](https://github.com/uber/uber-poet/actions/workflows/python-app.yml/badge.svg)](https://github.com/uber/uber-poet/actions) 5 | 6 | This python app makes mock Xcode Swift / ObjC app projects with [Buck](https://buckbuild.com/), [Bazel](https://bazel.build/) 7 | and [CocoaPods](https://cocoapods.org). It lets us test different Swift / ObjC module configurations to see how much build speed is affected by different [dependency graphs](docs/layer_types.md) with identical amounts of code. There are two main command line apps: 8 | 9 | * `genproj.py` which generates one app which you have to build manually yourself. Either with `buck`, `bazel` or `xcodebuild`. 10 | * `multisuite.py`, which generates all module configs, builds them, records how long they take to build into a CSV and outputs it's results to a directory passed in the command line. Essentially a benchmark test suite. Can take several hours to run depending how many lines of code each app takes. 11 | 12 | This app was architected so other languages, graph generators or build systems wouldn't be much work to add. Theoretically you could extend this app to generate java gradle android apps with the same [dependency graph types](docs/layer_types.md). 13 | 14 | ## How to Install / Dependencies 15 | 16 | With a mac computer that can run macOS 10.13+, install all the dependencies below: 17 | 18 | * macOS 10.13.X+, untested on older versions. 19 | * Python 2.7.X (pre-installed on macOS 10.13+) 20 | * Xcode command line tools & Xcode 21 | * Install with: `xcode-select --install` / [The mac app store](https://itunes.apple.com/us/app/xcode/id497799835) 22 | * [pipenv](https://pipenv.readthedocs.io/en/latest/) 23 | * Install with [homebrew](https://brew.sh): `brew install pipenv` 24 | * Optional: [cloc (Count Lines Of Code)](https://github.com/AlDanial/cloc) 25 | * Install with [homebrew](https://brew.sh): `brew install cloc` 26 | 27 | Depending on which project generator you plan to use, you will need to install at least one of the following: 28 | 29 | * [Buck](https://buckbuild.com/) 30 | * [Install instructions](https://buckbuild.com/setup/getting_started.html) 31 | * [Bazel](https://bazel.build/) 32 | * [Install instructions](https://docs.bazel.build/bazel-overview.html) 33 | * [CocoaPods](https://cocoapods.org/) 34 | * [Install instructions](https://cocoapods.org/#get_started) 35 | 36 | Then: 37 | 38 | * Download / git clone this project into a folder. 39 | * Run `pipenv install` to install the required python dependencies. 40 | * If you want to run unit tests or develop for this app, make sure to run `pipenv install --dev` 41 | 42 | ## How to Use 43 | 44 | After installing all required dependencies: 45 | 46 | See `pipenv run ./genproj.py -h` or `pipenv run ./mulisuite.py -h` for general help. Also take a look at the shell scripts in [examples/](examples/) to see examples on how to use these command line programs. 47 | 48 | Here a few quick examples: 49 | 50 | Generate a project using Buck: 51 | ```bash 52 | pipenv run ./genproj.py --output_directory "$HOME/Desktop/mockapp" \ 53 | --project_generator_type "buck" \ 54 | --blaze_module_path "/mockapp" \ 55 | --gen_type flat \ 56 | --swift_lines_of_code 150000 57 | ``` 58 | 59 | Generate a project using Bazel: 60 | ```bash 61 | pipenv run ./genproj.py --output_directory "$HOME/Desktop/mockapp" \ 62 | --project_generator_type "bazel" \ 63 | --blaze_module_path "/mockapp" \ 64 | --gen_type flat \ 65 | --swift_lines_of_code 150000 66 | ``` 67 | 68 | Generate a project using CocoaPods: 69 | ```bash 70 | pipenv run ./genproj.py --output_directory "$HOME/Desktop/mockapp" \ 71 | --project_generator_type "cocoapods" \ 72 | --gen_type flat \ 73 | --swift_lines_of_code 150000 74 | ``` 75 | 76 | You may also generate a project that includes both Swift and ObjC: 77 | 78 | ```bash 79 | pipenv run ./genproj.py --output_directory "$HOME/Desktop/mockapp" \ 80 | --project_generator_type "cocoapods" \ 81 | --gen_type flat \ 82 | --swift_lines_of_code 100000 \ 83 | --objc_lines_of_code 50000 84 | ``` 85 | 86 | ```bash 87 | # You usually want to use `caffeinate` to prevent your computer 88 | # from going to sleep during a multi hour build test suite. 89 | caffeinate -s pipenv run \ 90 | ./multisuite.py --log_dir "$HOME/Desktop/multisuite_build_results" \ 91 | --app_gen_output_dir "$HOME/Desktop/multisuite_build_results/app_gen" 92 | ``` 93 | 94 | You may also generate a project that matches your own project's dependency graph by using `--gen_type dot` parameter as well as supplying the location of the [`dot` file](https://en.wikipedia.org/wiki/DOT_(graph_description_language)) that represents the graph: 95 | 96 | ```bash 97 | pipenv run ./genproj.py --output_directory "$HOME/Desktop/mockapp" \ 98 | --project_generator_type "cocoapods" \ 99 | --gen_type dot \ 100 | --dot_file_path "$HOME/MyProject/my_project_graph.dot" \ 101 | --dot_root_node_name "MyProject" \ 102 | --swift_lines_of_code 150000 103 | ``` 104 | 105 | Examples on how to generate a `dot` file: 106 | 107 | 108 | Using Buck: 109 | ``` 110 | buck query \"deps(target)\" --dot > file.gv 111 | ``` 112 | 113 | Using Bazel: 114 | ``` 115 | bazel query "deps(target)" --output graph > graph.in 116 | ``` 117 | 118 | Using CocoaPods: 119 | 120 | Install and use the [cocoapods-dependencies](https://github.com/segiddins/cocoapods-dependencies) plugin. 121 | 122 | You may also supply an optional JSON file to be used as a LOC map. This allows you to generate a project from your own dependency graph in which each generated module has proportional LOC to your original graph. 123 | 124 | ```bash 125 | pipenv run ./genproj.py --output_directory "$HOME/Desktop/mockapp" \ 126 | --project_generator_type "cocoapods" \ 127 | --gen_type dot \ 128 | --dot_file_path "$HOME/MyProject/my_project_graph.dot" \ 129 | --dot_root_node_name "MyProject" \ 130 | --loc_json_file_path "$HOME/MyProject/cloc_mappings.json" 131 | ``` 132 | 133 | Please note the format of the JSON file for LOC mappings must look like: 134 | 135 | ```json 136 | { 137 | "MyLibrary":500, 138 | "MyOtherLibrary":42 139 | } 140 | ``` 141 | 142 | You may also specify a LOC mapping file that includes the language that you want to use for each module, for example: 143 | 144 | ```json 145 | { 146 | "MyLibrary": { "loc": 500, "language": "Objective-C" }, 147 | "MyOtherLibrary":42 148 | } 149 | ``` 150 | 151 | NOTE: All nodes found in your `dot` file must be present in your JSON LOC mappings file. 152 | 153 | Examples on how to get the CLOC: 154 | 155 | ``` 156 | cloc file.swift --include-lang="Swift" --json 157 | ``` 158 | 159 | Parse the JSON with your favorite language and read the `"code"` value from the `"SUM"` key. 160 | 161 | ## How to Contribute / Develop 162 | 163 | Take a look at [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md)! 164 | 165 | ## Project Status 166 | 167 | This project is stable and being incubated for long-term support. 168 | 169 | ## Licence 170 | 171 | This project is covered by the Apache License, Version 2.0: 172 | 173 | http://www.apache.org/licenses/LICENSE-2.0 174 | 175 | [LICENSE.txt](LICENSE.txt) 176 | -------------------------------------------------------------------------------- /tests/test_statemanagement.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Uber Technologies, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from __future__ import absolute_import 16 | 17 | import os.path 18 | import tempfile 19 | import unittest 20 | 21 | import mock 22 | import testfixtures.popen 23 | 24 | from uberpoet.statemanagement import SettingsState, XcodeManager, XcodeVersion 25 | 26 | from .utils import read_file, write_file 27 | 28 | 29 | def stdoutize(s): 30 | return b'{}\n'.format(s) 31 | 32 | 33 | class TestXcodeManager(unittest.TestCase): 34 | 35 | def setUp(self): 36 | self.mock_popen = testfixtures.popen.MockPopen() 37 | self.r = testfixtures.Replacer() 38 | self.r.replace('subprocess.Popen', self.mock_popen) 39 | self.addCleanup(self.r.restore) 40 | 41 | def test_get_dirs(self): 42 | x = XcodeManager() 43 | fake_paths = ['/Applications/Xcode-beta.app', '/Applications/Xcode.app', '/Applications/Chess.app'] 44 | expected_dirs = fake_paths[:2] 45 | 46 | with mock.patch('os.listdir') as mocked_listdir: 47 | mocked_listdir.return_value = fake_paths 48 | dirs = x.get_xcode_dirs() 49 | self.assertEqual(dirs, expected_dirs) 50 | 51 | def test_current_version(self): 52 | out = b'Xcode 10.0\nBuild version 10A255\n' 53 | self.mock_popen.set_command('xcodebuild -version', stdout=out) 54 | version, build = XcodeManager.get_current_xcode_version() 55 | self.assertEqual(version, '10.0') 56 | self.assertEqual(build, '10A255') 57 | 58 | def test_module_cache_dir(self): 59 | base = '/var/folders/sx/0zymnrm13ds1v5n9t4mjdqmr0000gp/C/' 60 | user = 'testuser' 61 | clangdir = 'org.llvm.clang.testuser' 62 | full_path = os.path.join(base, clangdir, 'ModuleCache') 63 | 64 | with mock.patch('getpass.getuser') as mock_getuser: 65 | mock_getuser.return_value = user 66 | self.mock_popen.set_command('getconf DARWIN_USER_CACHE_DIR', stdout=stdoutize(base)) 67 | self.assertEqual(full_path, XcodeManager()._get_global_module_cache_dir()) 68 | 69 | def test_clean_caches(self): 70 | self.mock_popen.set_default() 71 | XcodeManager().clean_caches() 72 | x = self.mock_popen.mock.method_calls 73 | self.assertEqual(len(x), 9) 74 | 75 | def test_discover_xcode_versions(self): 76 | # save_xcode_select / switch_xcode_version 77 | path = '/Applications/Xcode.app' 78 | self.mock_popen.set_command('xcode-select -p', stdout=b'{}\n'.format(path)) 79 | self.mock_popen.set_command('sudo xcode-select -s ' + path) 80 | 81 | # get_current_xcode_version 82 | out = b'Xcode 10.0\nBuild version 10A255\n' 83 | self.mock_popen.set_command('xcodebuild -version', stdout=out) 84 | 85 | # get_xcode_dirs 86 | fake_paths = ['/Applications/Xcode.app'] 87 | with mock.patch('os.listdir') as mocked_listdir: 88 | mocked_listdir.return_value = fake_paths 89 | versions = XcodeManager().discover_xcode_versions() 90 | expected = {('10.0', '10A255'): '/Applications/Xcode.app'} 91 | self.assertEqual(versions, expected) 92 | 93 | 94 | class TestSettingsState(unittest.TestCase): 95 | 96 | def setUp(self): 97 | self.mock_popen = testfixtures.popen.MockPopen() 98 | self.r = testfixtures.Replacer() 99 | self.r.replace('subprocess.Popen', self.mock_popen) 100 | self.addCleanup(self.r.restore) 101 | 102 | def test_buckconfig_restore(self): 103 | tmp = tempfile.gettempdir() 104 | conf_path = os.path.join(tmp, '.buckconfig.local') 105 | bak_conf_path = os.path.join(tmp, '.buckconfig.local.bak') 106 | config_content = 'a = b' 107 | new_config_content = '1 = 2' 108 | s = SettingsState(tmp) 109 | 110 | if os.path.isfile(conf_path): 111 | os.remove(conf_path) 112 | s.save_buckconfig_local() 113 | self.assertFalse(os.path.isfile(bak_conf_path)) # no file to save 114 | 115 | write_file(conf_path, config_content) 116 | 117 | for _ in xrange(4): 118 | s.save_buckconfig_local() 119 | self.assertTrue(os.path.isfile(bak_conf_path)) 120 | self.assertEqual(read_file(bak_conf_path), config_content) 121 | write_file(conf_path, new_config_content) 122 | self.assertEqual(read_file(conf_path), new_config_content) 123 | s.restore_buckconfig_local() 124 | self.assertEqual(read_file(conf_path), config_content) 125 | 126 | def test_xcode(self): 127 | path = '/Applications/Xcode.10.0.0.10A255.app/Contents/Developer' 128 | self.mock_popen.set_command('xcode-select -p', stdout=stdoutize(path)) 129 | self.mock_popen.set_command('sudo xcode-select -s ' + path) 130 | tmp = tempfile.gettempdir() 131 | s = SettingsState(tmp) 132 | 133 | s.save_xcode_select() 134 | self.assertEqual(s.select_path, path) 135 | s.restore_xcode_select() 136 | 137 | 138 | class TestXcodeVersion(unittest.TestCase): 139 | 140 | def test_choose_latest_major_versions_one_version(self): 141 | data = {('9.4.3', 'ASDF'): '/Applications/Xcode.9.4.3.app', ('10.0', 'QWERT'): '/Applications/Xcode-beta.app'} 142 | 143 | data2 = XcodeVersion.choose_latest_major_versions(data) 144 | 145 | self.assertEqual(data, data2) 146 | 147 | def test_repr(self): 148 | v = XcodeVersion('2.2.2', 'AAA') 149 | self.assertEqual(v.__repr__(), "XcodeVersion('2.2.2','AAA')") 150 | 151 | def test_choose_latest_major_versions_one_item(self): 152 | data = { 153 | ('9.4.3', 'ASDF'): '/Applications/Xcode.app', 154 | } 155 | 156 | data2 = XcodeVersion.choose_latest_major_versions(data) 157 | 158 | self.assertEqual(data, data2) 159 | 160 | def test_choose_latest_major_versions_multiple_majors(self): 161 | data = { 162 | ('9.5', 'ZZZZ'): '/Applications/Xcode.9.5.app', 163 | ('9.4.3', 'ASDF'): '/Applications/Xcode.9.4.3.app', 164 | ('9.2.3', 'JJJJ'): '/Applications/Xcode.9.2.3.app', 165 | ('10.0', 'AAAA'): '/Applications/Xcode-beta2.app', 166 | ('10.0', 'BBBB'): '/Applications/Xcode-beta.app', 167 | ('8.3.1', 'QWERT'): '/Applications/Xcode.8.3.1.app' 168 | } 169 | 170 | data_after = { 171 | ('9.5', 'ZZZZ'): '/Applications/Xcode.9.5.app', 172 | ('10.0', 'BBBB'): '/Applications/Xcode-beta.app', 173 | ('8.3.1', 'QWERT'): '/Applications/Xcode.8.3.1.app' 174 | } 175 | 176 | data2 = XcodeVersion.choose_latest_major_versions(data) 177 | 178 | self.assertEqual(data_after, data2) 179 | 180 | def test_xcode_equality(self): 181 | a = XcodeVersion('1.2.3', 'AAA') 182 | a2 = XcodeVersion('1.2.3', 'AAA') 183 | az = XcodeVersion('1.2.3', 'ZZZ') 184 | b = XcodeVersion('2.2.3', 'AAA') 185 | a124 = XcodeVersion('1.2.4', 'AAA') 186 | a125 = XcodeVersion('1.2.5', 'AAA') 187 | 188 | self.assertEqual(a, a2) 189 | self.assertEqual(a, a) 190 | self.assertNotEqual(a, az) 191 | self.assertNotEqual(a, b) 192 | self.assertNotEqual(b, a2) 193 | self.assertNotEqual(b, az) 194 | 195 | self.assertFalse(a124.__eq__(a125)) 196 | self.assertFalse(a.__eq__(az)) 197 | self.assertTrue(az.__gt__(a)) 198 | self.assertFalse(a.__gt__(az)) 199 | self.assertTrue(b.__gt__(a)) 200 | -------------------------------------------------------------------------------- /uberpoet/commandlineutil.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Uber Technologies, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from __future__ import absolute_import 16 | 17 | import ConfigParser 18 | import json 19 | import logging 20 | import os 21 | import shutil 22 | import subprocess 23 | from os.path import join 24 | 25 | from toposort import toposort_flatten 26 | 27 | from . import dotreader 28 | from .cpulogger import CPULog 29 | from .filegen import Language 30 | from .moduletree import ModuleGenType, ModuleNode 31 | from .util import bool_xor 32 | 33 | 34 | class AppGenerationConfig(object): 35 | 36 | def __init__(self, 37 | module_count=0, 38 | big_module_count=0, 39 | small_module_count=0, 40 | swift_lines_of_code=0, 41 | objc_lines_of_code=0, 42 | app_layer_count=0, 43 | dot_file_path='', 44 | dot_root_node_name='', 45 | loc_json_file_path=''): 46 | self.module_count = module_count 47 | self.big_module_count = big_module_count 48 | self.small_module_count = small_module_count 49 | self.swift_lines_of_code = swift_lines_of_code 50 | self.objc_lines_of_code = objc_lines_of_code 51 | self.app_layer_count = app_layer_count 52 | self.dot_file_path = dot_file_path 53 | self.dot_root_node_name = dot_root_node_name 54 | self.loc_json_file_path = loc_json_file_path 55 | 56 | def pull_from_args(self, args): 57 | self.validate_app_gen_options(args) 58 | self.module_count = args.module_count 59 | self.big_module_count = args.big_module_count 60 | self.small_module_count = args.small_module_count 61 | self.swift_lines_of_code = args.swift_lines_of_code 62 | self.objc_lines_of_code = args.objc_lines_of_code 63 | self.app_layer_count = args.app_layer_count 64 | self.dot_file_path = args.dot_file_path 65 | self.dot_root_node_name = args.dot_root_node_name 66 | self.loc_json_file_path = args.loc_json_file_path 67 | 68 | @staticmethod 69 | def add_app_gen_options(parser): 70 | app = parser.add_argument_group('Mock app generation options') 71 | app.add_argument( 72 | '--module_count', default=100, type=int, help="How many modules should be in a normal mock app type."), 73 | app.add_argument( 74 | '--big_module_count', 75 | default=3, 76 | type=int, 77 | help="How many big modules should be in a big/small mock app type."), 78 | app.add_argument( 79 | '--small_module_count', 80 | default=50, 81 | type=int, 82 | help="How many small modules should be in a big/small mock app type."), 83 | app.add_argument( 84 | '--swift_lines_of_code', 85 | default=1500000, # 1.5 million lines of code 86 | type=int, 87 | help="Approximately how many lines of Swift code each mock app should have."), 88 | app.add_argument( 89 | '--objc_lines_of_code', 90 | default=0, 91 | type=int, 92 | help="Approximately how many lines of ObjC code each mock app should have."), 93 | app.add_argument( 94 | '--app_layer_count', 95 | default=10, 96 | type=int, 97 | help='How many module layers there should be in the layered mock app type.') 98 | 99 | dot = parser.add_argument_group('Dot file mock app config') 100 | dot.add_argument( 101 | '--dot_file_path', 102 | default='', 103 | type=str, 104 | help="The path to the dot file to create a mock module graph from. This dot file for Buck can be " 105 | "created like so: `buck query \"deps(target)\" --dot > file.gv`. Alternatively, you may use your own" 106 | "means to generate it for different project types.") 107 | dot.add_argument( 108 | '--dot_root_node_name', 109 | default='', 110 | type=str, 111 | help="The name of the root application node of the dot file, such as 'App'.") 112 | parser.add_argument( 113 | '--loc_json_file_path', 114 | default='', 115 | type=str, 116 | help="A JSON file used to provide module LOC data. Only used when dot graph type is used to create " 117 | "modules with proportional LOC. You may generate this file using `cloc` or another tool like `tokei`." 118 | " The format of the file is expected to contain each module name as a key with a value denoting the LOC.") 119 | 120 | @staticmethod 121 | def validate_app_gen_options(args): 122 | if bool_xor(args.dot_file_path, args.dot_root_node_name): 123 | logging.info('dot_file_path: "%s" dot_root_node_name: "%s"', args.dot_file_path, args.dot_root_node_name) 124 | raise ValueError('If you specify a dot file config option, you must also specify a root node name using ' 125 | '\"dot_root_node_name\".') 126 | if args.loc_json_file_path and args.gen_type != ModuleGenType.dot: 127 | logging.info('loc_json_file_path: "%s"', args.loc_json_file_path) 128 | raise ValueError('If you specify \"loc_json_file_path\", you must also specify a dot graph style.') 129 | 130 | 131 | def gen_graph(gen_type, config): 132 | # app_node, node_list = None, None 133 | modules_per_layer = config.module_count / config.app_layer_count 134 | 135 | if gen_type == ModuleGenType.flat: 136 | app_node, node_list = ModuleNode.gen_flat_graph(config.module_count) 137 | elif gen_type == ModuleGenType.bs_flat: 138 | app_node, node_list = ModuleNode.gen_flat_big_small_graph(config.big_module_count, config.small_module_count) 139 | elif gen_type == ModuleGenType.layered: 140 | app_node, node_list = ModuleNode.gen_layered_graph(config.app_layer_count, modules_per_layer) 141 | elif gen_type == ModuleGenType.bs_layered: 142 | app_node, node_list = ModuleNode.gen_layered_big_small_graph(config.big_module_count, config.small_module_count) 143 | elif gen_type == ModuleGenType.dot and config.dot_file_path and config.dot_root_node_name: 144 | logging.info("Reading dot file: %s", config.dot_file_path) 145 | app_node, parsed_node_list = dotreader.DotFileReader().read_dot_file(config.dot_file_path, 146 | config.dot_root_node_name) 147 | node_graph = {n: set(n.deps) for n in parsed_node_list} 148 | node_list = toposort_flatten(node_graph) 149 | else: 150 | logging.error("Unexpected argument set, aborting.") 151 | item_list = ', '.join(ModuleGenType.enum_list()) 152 | logging.error("Choose from ({}) module count: {} dot path: {} ".format(item_list, config.module_count, 153 | config.dot_path)) 154 | raise ValueError("Invalid Arguments") 155 | 156 | return app_node, node_list 157 | 158 | 159 | def del_old_output_dir(output_directory): 160 | if os.path.isdir(output_directory): 161 | logging.warning("Deleting old mock app directory %s", output_directory) 162 | shutil.rmtree(output_directory) 163 | 164 | 165 | def make_custom_buckconfig_local(buckconfig_path): 166 | logging.warn('Overwriting .buckconfig.local file at: %s', buckconfig_path) 167 | config = ConfigParser.RawConfigParser() 168 | config.add_section('project') 169 | config.set('project', 'ide_force_kill', 'never') 170 | config.add_section('parser') 171 | config.set('parser', 'polyglot_parsing_enabled', 'true') 172 | config.set('parser', 'default_build_file_syntax', 'SKYLARK') 173 | 174 | with open(buckconfig_path, 'w') as buckconfig: 175 | config.write(buckconfig) 176 | 177 | 178 | def count_loc(code_path, language=Language.SWIFT): 179 | """Returns the number of lines of code in `code_path` using cloc. If cloc is not 180 | on your system or there is an error, then it returns -1""" 181 | try: 182 | logging.info('Counting lines of code in %s', code_path) 183 | raw_json_out = subprocess.check_output(['cloc', '--quiet', '--json', code_path]) 184 | except OSError: 185 | logging.warning("You do not have cloc installed, skipping line counting.") 186 | return -1 187 | 188 | json_out = json.loads(raw_json_out) 189 | language_loc = json_out.get(language, {}).get('code', 0) 190 | if not language_loc: 191 | logging.error('Unexpected cloc output "%s"', raw_json_out) 192 | raise ValueError('cloc did not give a correct value') 193 | return language_loc 194 | 195 | 196 | def apply_cpu_to_traces(build_trace_path, cpu_logger, time_cutoff=None): 197 | logging.info('Applying CPU info to traces in %s', build_trace_path) 198 | cpu_logs = cpu_logger.process_log() 199 | trace_paths = [join(build_trace_path, f) for f in os.listdir(build_trace_path) if f.endswith('trace')] 200 | for trace_path in trace_paths: 201 | if time_cutoff and os.path.getmtime(trace_path) < time_cutoff: 202 | continue 203 | with open(trace_path, 'r') as trace_file: 204 | traces = json.load(trace_file) 205 | new_traces = CPULog.apply_log_to_trace(cpu_logs, traces) 206 | with open(trace_path + '.json', 'w') as new_trace_file: 207 | json.dump(new_traces, new_trace_file) 208 | -------------------------------------------------------------------------------- /tests/fixtures/loc_mappings.json: -------------------------------------------------------------------------------- 1 | { 2 | "DotReaderLib0": { "loc": 180, "language": "Objective-C" }, 3 | "DotReaderLib1": 260, 4 | "DotReaderLib2": 210, 5 | "DotReaderLib3": 390, 6 | "DotReaderLib4": 220, 7 | "DotReaderLib5": 331, 8 | "DotReaderLib6": 17, 9 | "DotReaderLib7": 268, 10 | "DotReaderLib8": 496, 11 | "DotReaderLib9": 471, 12 | "DotReaderLib10": 129, 13 | "DotReaderLib11": 376, 14 | "DotReaderLib12": 330, 15 | "DotReaderLib13": 93, 16 | "DotReaderLib14": 342, 17 | "DotReaderLib15": 244, 18 | "DotReaderLib16": 419, 19 | "DotReaderLib17": 283, 20 | "DotReaderLib18": 341, 21 | "DotReaderLib19": 215, 22 | "DotReaderLib20": 161, 23 | "DotReaderLib21": 203, 24 | "DotReaderLib22": 239, 25 | "DotReaderLib23": 153, 26 | "DotReaderLib24": 355, 27 | "DotReaderLib25": 487, 28 | "DotReaderLib26": 287, 29 | "DotReaderLib27": 351, 30 | "DotReaderLib28": 333, 31 | "DotReaderLib29": 8, 32 | "DotReaderLib30": 336, 33 | "DotReaderLib31": 399, 34 | "DotReaderLib32": 12, 35 | "DotReaderLib33": 361, 36 | "DotReaderLib34": 394, 37 | "DotReaderLib35": 360, 38 | "DotReaderLib36": 341, 39 | "DotReaderLib37": 159, 40 | "DotReaderLib38": 387, 41 | "DotReaderLib39": 315, 42 | "DotReaderLib40": 165, 43 | "DotReaderLib41": 483, 44 | "DotReaderLib42": 379, 45 | "DotReaderLib43": 106, 46 | "DotReaderLib44": 230, 47 | "DotReaderLib45": 153, 48 | "DotReaderLib46": 155, 49 | "DotReaderLib47": 85, 50 | "DotReaderLib48": 407, 51 | "DotReaderLib49": 332, 52 | "DotReaderLib50": 241, 53 | "DotReaderLib51": 205, 54 | "DotReaderLib52": 14, 55 | "DotReaderLib53": 441, 56 | "DotReaderLib54": 146, 57 | "DotReaderLib55": 8, 58 | "DotReaderLib56": 25, 59 | "DotReaderLib57": 54, 60 | "DotReaderLib58": 143, 61 | "DotReaderLib59": 54, 62 | "DotReaderLib60": 14, 63 | "DotReaderLib61": 295, 64 | "DotReaderLib62": 181, 65 | "DotReaderLib63": 147, 66 | "DotReaderLib64": 439, 67 | "DotReaderLib65": 198, 68 | "DotReaderLib66": 76, 69 | "DotReaderLib67": 223, 70 | "DotReaderLib68": 395, 71 | "DotReaderLib69": 241, 72 | "DotReaderLib70": 214, 73 | "DotReaderLib71": 156, 74 | "DotReaderLib72": 216, 75 | "DotReaderLib73": 162, 76 | "DotReaderLib74": 245, 77 | "DotReaderLib75": 169, 78 | "DotReaderLib76": 364, 79 | "DotReaderLib77": 146, 80 | "DotReaderLib78": 229, 81 | "DotReaderLib79": 119, 82 | "DotReaderLib80": 287, 83 | "DotReaderLib81": 432, 84 | "DotReaderLib82": 101, 85 | "DotReaderLib83": 168, 86 | "DotReaderLib84": 99, 87 | "DotReaderLib85": 362, 88 | "DotReaderLib86": 341, 89 | "DotReaderLib87": 307, 90 | "DotReaderLib88": 200, 91 | "DotReaderLib89": 216, 92 | "DotReaderLib90": 17, 93 | "DotReaderLib91": 283, 94 | "DotReaderLib92": 46, 95 | "DotReaderLib93": 400, 96 | "DotReaderLib94": 358, 97 | "DotReaderLib95": 344, 98 | "DotReaderLib96": 384, 99 | "DotReaderLib97": 100, 100 | "DotReaderLib98": 308, 101 | "DotReaderLib99": 308, 102 | "DotReaderLib100": 361, 103 | "DotReaderLib101": 18, 104 | "DotReaderLib102": 462, 105 | "DotReaderLib103": 99, 106 | "DotReaderLib104": 453, 107 | "DotReaderLib105": 352, 108 | "DotReaderLib106": 228, 109 | "DotReaderLib107": 43, 110 | "DotReaderLib108": 452, 111 | "DotReaderLib109": 265, 112 | "DotReaderLib110": 10, 113 | "DotReaderLib111": 350, 114 | "DotReaderLib112": 355, 115 | "DotReaderLib113": 48, 116 | "DotReaderLib114": 124, 117 | "DotReaderLib115": 466, 118 | "DotReaderLib116": 161, 119 | "DotReaderLib117": 199, 120 | "DotReaderLib118": 28, 121 | "DotReaderLib119": 484, 122 | "DotReaderLib120": 149, 123 | "DotReaderLib121": 416, 124 | "DotReaderLib122": 174, 125 | "DotReaderLib123": 213, 126 | "DotReaderLib124": 205, 127 | "DotReaderLib125": 121, 128 | "DotReaderLib126": 335, 129 | "DotReaderLib127": 86, 130 | "DotReaderLib128": 42, 131 | "DotReaderLib129": 82, 132 | "DotReaderLib130": 184, 133 | "DotReaderLib131": 17, 134 | "DotReaderLib132": 426, 135 | "DotReaderLib133": 231, 136 | "DotReaderLib134": 406, 137 | "DotReaderLib135": 21, 138 | "DotReaderLib136": 364, 139 | "DotReaderLib137": 486, 140 | "DotReaderLib138": 15, 141 | "DotReaderLib139": 422, 142 | "DotReaderLib140": 30, 143 | "DotReaderLib141": 495, 144 | "DotReaderLib142": 386, 145 | "DotReaderLib143": 340, 146 | "DotReaderLib144": 205, 147 | "DotReaderLib145": 75, 148 | "DotReaderLib146": 45, 149 | "DotReaderLib147": 55, 150 | "DotReaderLib148": 310, 151 | "DotReaderLib149": 490, 152 | "DotReaderLib150": 45, 153 | "DotReaderLib151": 5, 154 | "DotReaderLib152": 194, 155 | "DotReaderLib153": 466, 156 | "DotReaderLib154": 58, 157 | "DotReaderLib155": 423, 158 | "DotReaderLib156": 234, 159 | "DotReaderLib157": 224, 160 | "DotReaderLib158": 240, 161 | "DotReaderLib159": 121, 162 | "DotReaderLib160": 384, 163 | "DotReaderLib161": 49, 164 | "DotReaderLib162": 240, 165 | "DotReaderLib163": 167, 166 | "DotReaderLib164": 227, 167 | "DotReaderLib165": 380, 168 | "DotReaderLib166": 65, 169 | "DotReaderLib167": 235, 170 | "DotReaderLib168": 251, 171 | "DotReaderLib169": 15, 172 | "DotReaderLib170": 118, 173 | "DotReaderLib171": 430, 174 | "DotReaderLib172": 383, 175 | "DotReaderLib173": 113, 176 | "DotReaderLib174": 20, 177 | "DotReaderLib175": 167, 178 | "DotReaderLib176": 7, 179 | "DotReaderLib177": 74, 180 | "DotReaderLib178": 159, 181 | "DotReaderLib179": 109, 182 | "DotReaderLib180": 403, 183 | "DotReaderLib181": 264, 184 | "DotReaderLib182": 37, 185 | "DotReaderLib183": 172, 186 | "DotReaderLib184": 209, 187 | "DotReaderLib185": 449, 188 | "DotReaderLib186": 143, 189 | "DotReaderLib187": 384, 190 | "DotReaderLib188": 26, 191 | "DotReaderLib189": 35, 192 | "DotReaderLib190": 320, 193 | "DotReaderLib191": 266, 194 | "DotReaderLib192": 401, 195 | "DotReaderLib193": 333, 196 | "DotReaderLib194": 318, 197 | "DotReaderLib195": 402, 198 | "DotReaderLib196": 315, 199 | "DotReaderLib197": 77, 200 | "DotReaderLib198": 323, 201 | "DotReaderLib199": 488, 202 | "DotReaderLib200": 30, 203 | "DotReaderLib201": 82, 204 | "DotReaderLib202": 153, 205 | "DotReaderLib203": 177, 206 | "DotReaderLib204": 397, 207 | "DotReaderLib205": 417, 208 | "DotReaderLib206": 350, 209 | "DotReaderLib207": 240, 210 | "DotReaderLib208": 425, 211 | "DotReaderLib209": 439, 212 | "DotReaderLib210": 204, 213 | "DotReaderLib211": 400, 214 | "DotReaderLib212": 341, 215 | "DotReaderLib213": 425, 216 | "DotReaderLib214": 316, 217 | "DotReaderLib215": 438, 218 | "DotReaderLib216": 11, 219 | "DotReaderLib217": 86, 220 | "DotReaderLib218": 489, 221 | "DotReaderLib219": 355, 222 | "DotReaderLib220": 439, 223 | "DotReaderLib221": 243, 224 | "DotReaderLib222": 453, 225 | "DotReaderLib223": 382, 226 | "DotReaderLib224": 355, 227 | "DotReaderLib225": 298, 228 | "DotReaderLib226": 305, 229 | "DotReaderLib227": 48, 230 | "DotReaderLib228": 86, 231 | "DotReaderLib229": 1, 232 | "DotReaderLib230": 90, 233 | "DotReaderLib231": 225, 234 | "DotReaderLib232": 116, 235 | "DotReaderLib233": 306, 236 | "DotReaderLib234": 447, 237 | "DotReaderLib235": 299, 238 | "DotReaderLib236": 84, 239 | "DotReaderLib237": 63, 240 | "DotReaderLib238": 395, 241 | "DotReaderLib239": 155, 242 | "DotReaderLib240": 488, 243 | "DotReaderLib241": 163, 244 | "DotReaderLib242": 477, 245 | "DotReaderLib243": 11, 246 | "DotReaderLib244": 87, 247 | "DotReaderLib245": 51, 248 | "DotReaderLib246": 414, 249 | "DotReaderLib247": 5, 250 | "DotReaderLib248": 198, 251 | "DotReaderLib249": 201, 252 | "DotReaderLib250": 483, 253 | "DotReaderLib251": 255, 254 | "DotReaderLib252": 401, 255 | "DotReaderLib253": 158, 256 | "DotReaderLib254": 414, 257 | "DotReaderLib255": 14, 258 | "DotReaderLib256": 258, 259 | "DotReaderLib257": 296, 260 | "DotReaderLib258": 118, 261 | "DotReaderLib259": 117, 262 | "DotReaderLib260": 212, 263 | "DotReaderLib261": 454, 264 | "DotReaderLib262": 199, 265 | "DotReaderLib263": 335, 266 | "DotReaderLib264": 1, 267 | "DotReaderLib265": 201, 268 | "DotReaderLib266": 141, 269 | "DotReaderLib267": 342, 270 | "DotReaderLib268": 27, 271 | "DotReaderLib269": 113, 272 | "DotReaderLib270": 4, 273 | "DotReaderLib271": 305, 274 | "DotReaderLib272": 194, 275 | "DotReaderLib273": 369, 276 | "DotReaderLib274": 1, 277 | "DotReaderLib275": 345, 278 | "DotReaderLib276": 313, 279 | "DotReaderLib277": 358, 280 | "DotReaderLib278": 221, 281 | "DotReaderLib279": 60, 282 | "DotReaderLib280": 199, 283 | "DotReaderLib281": 489, 284 | "DotReaderLib282": 320, 285 | "DotReaderLib283": 378, 286 | "DotReaderLib284": 100, 287 | "DotReaderLib285": 181, 288 | "DotReaderLib286": 159, 289 | "DotReaderLib287": 442, 290 | "DotReaderLib288": 248, 291 | "DotReaderLib289": 73, 292 | "DotReaderLib290": 382, 293 | "DotReaderLib291": 209, 294 | "DotReaderLib292": 217, 295 | "DotReaderLib293": 28, 296 | "DotReaderLib294": 346, 297 | "DotReaderLib295": 421, 298 | "DotReaderLib296": 416, 299 | "DotReaderLib297": 279, 300 | "DotReaderLib298": 430, 301 | "DotReaderLib299": 451, 302 | "DotReaderLib300": 196, 303 | "DotReaderLib301": 303, 304 | "DotReaderLib302": 308, 305 | "DotReaderLib303": 385, 306 | "DotReaderLib304": 145, 307 | "DotReaderLib305": 371, 308 | "DotReaderLib306": 83, 309 | "DotReaderLib307": 59, 310 | "DotReaderLib308": 127, 311 | "DotReaderLib309": 317, 312 | "DotReaderLib310": 45, 313 | "DotReaderLib311": 345, 314 | "DotReaderLib312": 270, 315 | "DotReaderLib313": 380, 316 | "DotReaderLib314": 67, 317 | "DotReaderLib315": 119, 318 | "DotReaderLib316": 448, 319 | "DotReaderLib317": 455, 320 | "DotReaderLib318": 253, 321 | "DotReaderLib319": 302, 322 | "DotReaderLib320": 463, 323 | "DotReaderLib321": 171, 324 | "DotReaderLib322": 324, 325 | "DotReaderLib323": 459, 326 | "DotReaderLib324": 179, 327 | "DotReaderLib325": 345, 328 | "DotReaderLib326": 471, 329 | "DotReaderLib327": 417, 330 | "DotReaderLib328": 283, 331 | "DotReaderLib329": 147, 332 | "DotReaderLib330": 387, 333 | "DotReaderLib331": 204, 334 | "DotReaderLib332": 415, 335 | "DotReaderLib333": 265, 336 | "DotReaderLib334": 58, 337 | "DotReaderLib335": 426, 338 | "DotReaderLib336": 476 339 | } 340 | -------------------------------------------------------------------------------- /tests/test_buck_integration.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Uber Technologies, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from __future__ import absolute_import 16 | 17 | import os 18 | import tempfile 19 | import unittest 20 | from os.path import join 21 | 22 | import mock 23 | import testfixtures.popen 24 | from testfixtures.popen import MockPopen, PopenBehaviour 25 | 26 | from uberpoet.filegen import Language 27 | from uberpoet.genproj import GenProjCommandLine 28 | from uberpoet.multisuite import CommandLineMultisuite 29 | 30 | from .utils import integration_test, read_file 31 | 32 | 33 | class TestBuckIntegration(unittest.TestCase): 34 | 35 | def verify_genproj(self, app_path, dir_file_count, swift_file_count, objc_file_count): 36 | main_path = join(app_path, 'App') 37 | 38 | # Top level dir 39 | contents = os.listdir(app_path) 40 | self.assertGreater(len(contents), 0) 41 | self.assertEqual(len(contents), dir_file_count) 42 | 43 | swift_file_contents = [] 44 | objc_file_contents = [] 45 | 46 | for dirpath, dirnames, filenames in os.walk(app_path): 47 | for f in filenames: 48 | ext = os.path.splitext(f)[1] 49 | if ext == '.swift': 50 | swift_file_contents.append(os.path.join(dirpath, f)) 51 | elif ext == '.h' or ext == '.m': 52 | objc_file_contents.append(os.path.join(dirpath, f)) 53 | 54 | self.assertEqual(len(swift_file_contents), swift_file_count) 55 | self.assertEqual(len(objc_file_contents), objc_file_count) 56 | 57 | # App dir 58 | self.assertIn('App', contents) 59 | app_contents = os.listdir(main_path) 60 | self.assertGreater(len(app_contents), 0) 61 | for file_name in ['BUCK', 'AppDelegate.swift', 'Info.plist']: 62 | self.assertIn(file_name, app_contents) 63 | with open(join(main_path, file_name), 'r') as f: 64 | self.assertGreater(len(f.read()), 0) 65 | # TODO actually verify generated code? 66 | 67 | def verify_lib(self, app_path, lib_name, language=Language.SWIFT): 68 | lib_path = join(app_path, lib_name, 'Sources') 69 | 70 | lib_contents = os.listdir(lib_path) 71 | self.assertGreater(len(lib_contents), 0) 72 | 73 | if language == Language.SWIFT: 74 | files = list(['File0.swift']) 75 | elif language == Language.OBJC: 76 | files = list(['File0.h', 'File0.m']) 77 | 78 | for f in files: 79 | self.assertIn(f, lib_contents) 80 | with open(join(lib_path, f), 'r') as f: 81 | self.assertGreater(len(f.read()), 0) 82 | # TODO actually verify generated code? 83 | 84 | @integration_test 85 | def test_flat_genproj(self): 86 | app_path = join(tempfile.gettempdir(), 'apps', 'mockapp') 87 | args = [ 88 | "--output_directory", app_path, "--blaze_module_path", "/apps/mockapp", "--gen_type", "flat", 89 | "--swift_lines_of_code", "150000" 90 | ] 91 | command = GenProjCommandLine() 92 | command.main(args) 93 | 94 | self.verify_genproj(app_path, 104, 901, 0) 95 | self.verify_lib(app_path, 'MockLib53') 96 | 97 | @integration_test 98 | def test_genproj_with_objc(self): 99 | app_path = join(tempfile.gettempdir(), 'apps', 'mockapp') 100 | args = [ 101 | "--output_directory", app_path, "--blaze_module_path", "/apps/mockapp", "--gen_type", "flat", 102 | "--swift_lines_of_code", "0", 103 | "--objc_lines_of_code", "150000" 104 | ] 105 | command = GenProjCommandLine() 106 | command.main(args) 107 | # 1 Swift file count expected due to main.swift for the app target. 108 | self.verify_genproj(app_path, 104, 1, 2200) 109 | self.verify_lib(app_path, 'MockLib53', Language.OBJC) 110 | 111 | @integration_test 112 | def test_dot_genproj(self): 113 | app_path = join(tempfile.gettempdir(), 'apps', 'mockapp') 114 | 115 | test_fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures', 'test_dot.gv') 116 | args = [ 117 | "--output_directory", app_path, "--blaze_module_path", "/apps/mockapp", "--gen_type", "dot", 118 | "--swift_lines_of_code", "150000", "--dot_file", test_fixture_path, "--dot_root", "DotReaderMainModule" 119 | ] 120 | command = GenProjCommandLine() 121 | command.main(args) 122 | 123 | self.verify_genproj(app_path, 341, 675, 0) 124 | self.verify_lib(app_path, 'DotReaderLib17') 125 | 126 | @integration_test 127 | def test_dot_genproj_with_loc_mappings(self): 128 | app_path = join(tempfile.gettempdir(), 'apps', 'mockapp') 129 | 130 | test_fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures', 'test_dot.gv') 131 | loc_test_fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures', 'loc_mappings.json') 132 | args = [ 133 | "--output_directory", app_path, "--blaze_module_path", "/apps/mockapp", "--gen_type", "dot", 134 | "--dot_file", test_fixture_path, "--dot_root", "DotReaderMainModule", 135 | "--loc_json_file_path", loc_test_fixture_path 136 | ] 137 | command = GenProjCommandLine() 138 | command.main(args) 139 | 140 | self.verify_genproj(app_path, 342, 484, 2) 141 | self.verify_lib(app_path, 'DotReaderLib17') 142 | 143 | @integration_test 144 | def test_flat_multisuite(self): 145 | root_path = join(tempfile.gettempdir(), 'multisuite_test') 146 | app_path = join(root_path, 'apps', 'mockapp') 147 | log_path = join(root_path, 'logs') 148 | args = ["--log_dir", log_path, "--app_gen_output_dir", root_path, "--test_build_only", "--skip_xcode_build"] 149 | command = CommandLineMultisuite() 150 | command.main(args) 151 | self.assertGreater(os.listdir(app_path), 0) 152 | self.verify_genproj(app_path, 103, 601, 0) 153 | self.verify_lib(app_path, 'MockLib53') 154 | 155 | @integration_test 156 | def test_flat_multisuite_mocking_calls(self): 157 | test_cloc_out_path = os.path.join(os.path.dirname(__file__), 'fixtures', 'cloc_out.json') 158 | cloc_out = read_file(test_cloc_out_path) 159 | root_path = join(tempfile.gettempdir(), 'multisuite_test') 160 | app_path = join(root_path, 'apps', 'mockapp') 161 | log_path = join(root_path, 'logs') 162 | args = [ 163 | "--log_dir", log_path, "--app_gen_output_dir", root_path, "--test_build_only", "--switch_xcode_versions", 164 | "--full_clean" 165 | ] 166 | 167 | # we need the unused named variable for mocking purposes 168 | # noinspection PyUnusedLocal 169 | def command_callable(command, stdin): 170 | if 'cloc' in command: 171 | return PopenBehaviour(stdout=cloc_out) 172 | elif 'xcodebuild -version' in command: 173 | return PopenBehaviour(stdout=b'Xcode 10.0\nBuild version 10A255\n') 174 | return PopenBehaviour(stdout=b'test_out', stderr=b'test_error') 175 | 176 | with testfixtures.Replacer() as rep: 177 | mock_popen = MockPopen() 178 | rep.replace('subprocess.Popen', mock_popen) 179 | mock_popen.set_default(behaviour=command_callable) 180 | 181 | with mock.patch('distutils.spawn.find_executable') as mock_find: 182 | mock_find.return_value = '/bin/ls' # A non empty return value basically means "I found that executable" 183 | CommandLineMultisuite().main(args) 184 | self.assertGreater(os.listdir(app_path), 0) 185 | self.verify_genproj(app_path, 103, 601, 0) 186 | self.verify_lib(app_path, 'MockLib53') 187 | 188 | @integration_test 189 | def test_dot_multisuite(self): 190 | test_fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures', 'test_dot.gv') 191 | root_path = join(tempfile.gettempdir(), 'multisuite_test') 192 | app_path = join(root_path, 'apps', 'mockapp') 193 | log_path = join(root_path, 'logs') 194 | args = [ 195 | "--log_dir", log_path, "--app_gen_output_dir", root_path, "--dot_file", test_fixture_path, "--dot_root", 196 | "DotReaderMainModule", "--skip_xcode_build", "--test_build_only" 197 | ] 198 | command = CommandLineMultisuite() 199 | command.main(args) 200 | self.assertGreater(os.listdir(app_path), 0) 201 | self.verify_genproj(app_path, 340, 338, 0) 202 | self.verify_lib(app_path, 'DotReaderLib17') 203 | 204 | @integration_test 205 | def test_all_multisuite(self): 206 | test_fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures', 'test_dot.gv') 207 | root_path = join(tempfile.gettempdir(), 'multisuite_test') 208 | app_path = join(root_path, 'apps', 'mockapp') 209 | log_path = join(root_path, 'logs') 210 | args = [ 211 | "--log_dir", log_path, "--app_gen_output_dir", root_path, "--dot_file", test_fixture_path, "--dot_root", 212 | "DotReaderMainModule", "--skip_xcode_build", "--swift_lines_of_code", "150000" 213 | ] 214 | command = CommandLineMultisuite() 215 | command.main(args) 216 | self.assertGreater(os.listdir(app_path), 0) 217 | 218 | # Note we are assuming that the last project to be generated is the dot project. 219 | # If you change the order of project generation, make this match whatever is the new 'last project' 220 | # It's a bit fragile, but it's better than not verifying anything currently 221 | self.verify_genproj(app_path, 340, 675, 0) 222 | self.verify_lib(app_path, 'DotReaderLib17') 223 | -------------------------------------------------------------------------------- /tests/test_cp_integration.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Uber Technologies, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from __future__ import absolute_import 16 | 17 | import os 18 | import tempfile 19 | import unittest 20 | from os.path import join 21 | 22 | import mock 23 | import testfixtures.popen 24 | from testfixtures.popen import MockPopen, PopenBehaviour 25 | 26 | from uberpoet.filegen import Language 27 | from uberpoet.genproj import GenProjCommandLine 28 | from uberpoet.multisuite import CommandLineMultisuite 29 | 30 | from .utils import integration_test, read_file 31 | 32 | 33 | class TestCocoaPodsIntegration(unittest.TestCase): 34 | def verify_genproj(self, app_path, dir_file_count, swift_file_count, objc_file_count): 35 | main_path = join(app_path, 'App') 36 | 37 | # Top level dir 38 | contents = os.listdir(app_path) 39 | self.assertGreater(len(contents), 0) 40 | self.assertEqual(len(contents), dir_file_count) 41 | 42 | swift_file_contents = [] 43 | objc_file_contents = [] 44 | 45 | for dirpath, dirnames, filenames in os.walk(app_path): 46 | for f in filenames: 47 | ext = os.path.splitext(f)[1] 48 | if ext == '.swift': 49 | swift_file_contents.append(os.path.join(dirpath, f)) 50 | elif ext == '.h' or ext == '.m': 51 | objc_file_contents.append(os.path.join(dirpath, f)) 52 | 53 | self.assertEqual(len(swift_file_contents), swift_file_count) 54 | self.assertEqual(len(objc_file_contents), objc_file_count) 55 | 56 | # App dir 57 | self.assertIn('App', contents) 58 | app_contents = os.listdir(main_path) 59 | self.assertGreater(len(app_contents), 0) 60 | for file_name in ['AppContainer.podspec', 'AppDelegate.swift', 'Info.plist']: 61 | self.assertIn(file_name, app_contents) 62 | with open(join(main_path, file_name), 'r') as f: 63 | self.assertGreater(len(f.read()), 0) 64 | # TODO actually verify generated code? 65 | 66 | def verify_lib(self, app_path, lib_name, language=Language.SWIFT): 67 | lib_path = join(app_path, lib_name, 'Sources') 68 | 69 | lib_contents = os.listdir(lib_path) 70 | self.assertGreater(len(lib_contents), 0) 71 | 72 | if language == Language.SWIFT: 73 | files = list(['File0.swift']) 74 | elif language == Language.OBJC: 75 | files = list(['File0.h', 'File0.m']) 76 | 77 | for f in files: 78 | self.assertIn(f, lib_contents) 79 | with open(join(lib_path, f), 'r') as f: 80 | self.assertGreater(len(f.read()), 0) 81 | # TODO actually verify generated code? 82 | 83 | @integration_test 84 | def test_flat_genproj(self): 85 | app_path = join(tempfile.gettempdir(), 'apps', 'mockapp') 86 | args = [ 87 | "--output_directory", app_path, "--project_generator_type", "cocoapods", "--gen_type", "flat", 88 | "--swift_lines_of_code", "150000" 89 | ] 90 | command = GenProjCommandLine() 91 | command.main(args) 92 | 93 | self.verify_genproj(app_path, 104, 902, 0) 94 | self.verify_lib(app_path, 'MockLib53') 95 | 96 | @integration_test 97 | def test_genproj_with_objc(self): 98 | app_path = join(tempfile.gettempdir(), 'apps', 'mockapp') 99 | args = [ 100 | "--output_directory", app_path, "--project_generator_type", "cocoapods", "--gen_type", "flat", 101 | "--swift_lines_of_code", "0", "--objc_lines_of_code", "150000" 102 | ] 103 | command = GenProjCommandLine() 104 | command.main(args) 105 | 106 | # 2 Swift file count expected due to main.swift and dummy.swift files for the app target. 107 | self.verify_genproj(app_path, 104, 2, 2200) 108 | self.verify_lib(app_path, 'MockLib53', Language.OBJC) 109 | 110 | @integration_test 111 | def test_dot_genproj(self): 112 | app_path = join(tempfile.gettempdir(), 'apps', 'mockapp') 113 | 114 | test_fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures', 'test_dot.gv') 115 | args = [ 116 | "--output_directory", app_path, "--project_generator_type", "cocoapods", "--gen_type", "dot", 117 | "--swift_lines_of_code", "150000", "--dot_file", test_fixture_path, "--dot_root", "DotReaderMainModule" 118 | ] 119 | command = GenProjCommandLine() 120 | command.main(args) 121 | 122 | self.verify_genproj(app_path, 341, 676, 0) 123 | self.verify_lib(app_path, 'DotReaderLib17') 124 | 125 | @integration_test 126 | def test_dot_genproj_with_loc_mappings(self): 127 | app_path = join(tempfile.gettempdir(), 'apps', 'mockapp') 128 | print(app_path) 129 | 130 | test_fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures', 'test_dot.gv') 131 | loc_test_fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures', 'loc_mappings.json') 132 | args = [ 133 | "--output_directory", app_path, "--project_generator_type", "cocoapods", "--gen_type", "dot", 134 | "--dot_file", test_fixture_path, "--dot_root", "DotReaderMainModule", 135 | "--loc_json_file_path", loc_test_fixture_path 136 | ] 137 | command = GenProjCommandLine() 138 | command.main(args) 139 | 140 | self.verify_genproj(app_path, 342, 485, 2) 141 | self.verify_lib(app_path, 'DotReaderLib17') 142 | 143 | @integration_test 144 | def test_flat_multisuite(self): 145 | root_path = join(tempfile.gettempdir(), 'multisuite_test') 146 | app_path = join(root_path, 'apps', 'mockapp') 147 | log_path = join(root_path, 'logs') 148 | args = [ 149 | "--log_dir", log_path, "--app_gen_output_dir", root_path, "--test_build_only", "--skip_xcode_build", 150 | "--project_generator_type", "cocoapods" 151 | ] 152 | command = CommandLineMultisuite() 153 | command.main(args) 154 | self.assertGreater(os.listdir(app_path), 0) 155 | self.verify_genproj(app_path, 103, 602, 0) 156 | self.verify_lib(app_path, 'MockLib53') 157 | 158 | @integration_test 159 | def test_flat_multisuite_mocking_calls(self): 160 | test_cloc_out_path = os.path.join(os.path.dirname(__file__), 'fixtures', 'cloc_out.json') 161 | cloc_out = read_file(test_cloc_out_path) 162 | root_path = join(tempfile.gettempdir(), 'multisuite_test') 163 | app_path = join(root_path, 'apps', 'mockapp') 164 | log_path = join(root_path, 'logs') 165 | args = [ 166 | "--log_dir", log_path, "--app_gen_output_dir", root_path, "--test_build_only", "--switch_xcode_versions", 167 | "--full_clean", "--project_generator_type", "cocoapods" 168 | ] 169 | 170 | # we need the unused named variable for mocking purposes 171 | # noinspection PyUnusedLocal 172 | def command_callable(command, stdin): 173 | if 'cloc' in command: 174 | return PopenBehaviour(stdout=cloc_out) 175 | elif 'xcodebuild -version' in command: 176 | return PopenBehaviour(stdout=b'Xcode 10.0\nBuild version 10A255\n') 177 | return PopenBehaviour(stdout=b'test_out', stderr=b'test_error') 178 | 179 | with testfixtures.Replacer() as rep: 180 | mock_popen = MockPopen() 181 | rep.replace('subprocess.Popen', mock_popen) 182 | mock_popen.set_default(behaviour=command_callable) 183 | 184 | with mock.patch('distutils.spawn.find_executable') as mock_find: 185 | mock_find.return_value = '/bin/ls' # A non empty return value basically means "I found that executable" 186 | CommandLineMultisuite().main(args) 187 | self.assertGreater(os.listdir(app_path), 0) 188 | self.verify_genproj(app_path, 103, 602, 0) 189 | self.verify_lib(app_path, 'MockLib53') 190 | 191 | @integration_test 192 | def test_dot_multisuite(self): 193 | test_fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures', 'test_dot.gv') 194 | root_path = join(tempfile.gettempdir(), 'multisuite_test') 195 | app_path = join(root_path, 'apps', 'mockapp') 196 | log_path = join(root_path, 'logs') 197 | args = [ 198 | "--log_dir", log_path, "--app_gen_output_dir", root_path, "--dot_file", test_fixture_path, "--dot_root", 199 | "DotReaderMainModule", "--skip_xcode_build", "--test_build_only", "--project_generator_type", "cocoapods" 200 | ] 201 | command = CommandLineMultisuite() 202 | command.main(args) 203 | self.assertGreater(os.listdir(app_path), 0) 204 | self.verify_genproj(app_path, 340, 339, 0) 205 | self.verify_lib(app_path, 'DotReaderLib17') 206 | 207 | @integration_test 208 | def test_all_multisuite(self): 209 | test_fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures', 'test_dot.gv') 210 | root_path = join(tempfile.gettempdir(), 'multisuite_test') 211 | app_path = join(root_path, 'apps', 'mockapp') 212 | log_path = join(root_path, 'logs') 213 | args = [ 214 | "--log_dir", log_path, "--app_gen_output_dir", root_path, "--dot_file", test_fixture_path, "--dot_root", 215 | "DotReaderMainModule", "--skip_xcode_build", "--swift_lines_of_code", "150000", 216 | "--project_generator_type", "cocoapods" 217 | ] 218 | command = CommandLineMultisuite() 219 | command.main(args) 220 | self.assertGreater(os.listdir(app_path), 0) 221 | 222 | # Note we are assuming that the last project to be generated is the dot project. 223 | # If you change the order of project generation, make this match whatever is the new 'last project' 224 | # It's a bit fragile, but it's better than not verifying anything currently 225 | self.verify_genproj(app_path, 340, 676, 0) 226 | self.verify_lib(app_path, 'DotReaderLib17') 227 | -------------------------------------------------------------------------------- /tests/test_bazel_integration.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Uber Technologies, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from __future__ import absolute_import 16 | 17 | import os 18 | import tempfile 19 | import unittest 20 | from os.path import join 21 | 22 | import mock 23 | import testfixtures.popen 24 | from testfixtures.popen import MockPopen, PopenBehaviour 25 | 26 | from uberpoet.filegen import Language 27 | from uberpoet.genproj import GenProjCommandLine 28 | from uberpoet.multisuite import CommandLineMultisuite 29 | 30 | from .utils import integration_test, read_file 31 | 32 | 33 | class TestBazelIntegration(unittest.TestCase): 34 | 35 | def verify_genproj(self, app_path, dir_file_count, swift_file_count, objc_file_count): 36 | main_path = join(app_path, 'App') 37 | 38 | # Top level dir 39 | contents = os.listdir(app_path) 40 | self.assertGreater(len(contents), 0) 41 | self.assertEqual(len(contents), dir_file_count) 42 | 43 | swift_file_contents = [] 44 | objc_file_contents = [] 45 | 46 | for dirpath, dirnames, filenames in os.walk(app_path): 47 | for f in filenames: 48 | ext = os.path.splitext(f)[1] 49 | if ext == '.swift': 50 | swift_file_contents.append(os.path.join(dirpath, f)) 51 | elif ext == '.h' or ext == '.m': 52 | objc_file_contents.append(os.path.join(dirpath, f)) 53 | 54 | self.assertEqual(len(swift_file_contents), swift_file_count) 55 | self.assertEqual(len(objc_file_contents), objc_file_count) 56 | 57 | # App dir 58 | self.assertIn('App', contents) 59 | app_contents = os.listdir(main_path) 60 | self.assertGreater(len(app_contents), 0) 61 | for file_name in ['BUILD', 'AppDelegate.swift', 'Info.plist']: 62 | self.assertIn(file_name, app_contents) 63 | with open(join(main_path, file_name), 'r') as f: 64 | self.assertGreater(len(f.read()), 0) 65 | # TODO actually verify generated code? 66 | 67 | def verify_lib(self, app_path, lib_name, language=Language.SWIFT): 68 | lib_path = join(app_path, lib_name, 'Sources') 69 | 70 | lib_contents = os.listdir(lib_path) 71 | self.assertGreater(len(lib_contents), 0) 72 | 73 | if language == Language.SWIFT: 74 | files = list(['File0.swift']) 75 | elif language == Language.OBJC: 76 | files = list(['File0.h', 'File0.m']) 77 | 78 | for f in files: 79 | self.assertIn(f, lib_contents) 80 | with open(join(lib_path, f), 'r') as f: 81 | self.assertGreater(len(f.read()), 0) 82 | # TODO actually verify generated code? 83 | 84 | @integration_test 85 | def test_flat_genproj(self): 86 | app_path = join(tempfile.gettempdir(), 'apps', 'mockapp') 87 | args = [ 88 | "--output_directory", app_path, "--blaze_module_path", "/apps/mockapp", "--gen_type", "flat", 89 | "--project_generator_type", "bazel", 90 | "--swift_lines_of_code", "150000" 91 | ] 92 | command = GenProjCommandLine() 93 | command.main(args) 94 | 95 | self.verify_genproj(app_path, 105, 901, 0) 96 | self.verify_lib(app_path, 'MockLib53') 97 | 98 | @integration_test 99 | def test_genproj_with_objc(self): 100 | app_path = join(tempfile.gettempdir(), 'apps', 'mockapp') 101 | args = [ 102 | "--output_directory", app_path, "--blaze_module_path", "/apps/mockapp", "--gen_type", "flat", 103 | "--project_generator_type", "bazel", 104 | "--swift_lines_of_code", "0", 105 | "--objc_lines_of_code", "150000" 106 | ] 107 | command = GenProjCommandLine() 108 | command.main(args) 109 | 110 | # 1 Swift file count expected due to main.swift for the app target. 111 | self.verify_genproj(app_path, 105, 1, 2200) 112 | self.verify_lib(app_path, 'MockLib53', Language.OBJC) 113 | 114 | @integration_test 115 | def test_dot_genproj(self): 116 | app_path = join(tempfile.gettempdir(), 'apps', 'mockapp') 117 | 118 | test_fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures', 'test_dot.gv') 119 | args = [ 120 | "--output_directory", app_path, "--blaze_module_path", "/apps/mockapp", "--gen_type", "dot", 121 | "--project_generator_type", "bazel", 122 | "--swift_lines_of_code", "150000", "--dot_file", test_fixture_path, "--dot_root", "DotReaderMainModule" 123 | ] 124 | command = GenProjCommandLine() 125 | command.main(args) 126 | 127 | self.verify_genproj(app_path, 342, 675, 0) 128 | self.verify_lib(app_path, 'DotReaderLib17') 129 | 130 | @integration_test 131 | def test_dot_genproj_with_loc_mappings(self): 132 | app_path = join(tempfile.gettempdir(), 'apps', 'mockapp') 133 | 134 | test_fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures', 'test_dot.gv') 135 | loc_test_fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures', 'loc_mappings.json') 136 | args = [ 137 | "--output_directory", app_path, "--blaze_module_path", "/apps/mockapp", "--gen_type", "dot", 138 | "--project_generator_type", "bazel", 139 | "--dot_file", test_fixture_path, "--dot_root", "DotReaderMainModule", 140 | "--loc_json_file_path", loc_test_fixture_path 141 | ] 142 | command = GenProjCommandLine() 143 | command.main(args) 144 | 145 | self.verify_genproj(app_path, 343, 484, 2) 146 | self.verify_lib(app_path, 'DotReaderLib17') 147 | 148 | @integration_test 149 | def test_flat_multisuite(self): 150 | root_path = join(tempfile.gettempdir(), 'multisuite_test') 151 | app_path = join(root_path, 'apps', 'mockapp') 152 | log_path = join(root_path, 'logs') 153 | args = ["--log_dir", log_path, "--app_gen_output_dir", root_path, "--test_build_only", "--skip_xcode_build", 154 | "--project_generator_type", "bazel"] 155 | command = CommandLineMultisuite() 156 | command.main(args) 157 | self.assertGreater(os.listdir(app_path), 0) 158 | self.verify_genproj(app_path, 104, 601, 0) 159 | self.verify_lib(app_path, 'MockLib53') 160 | 161 | @integration_test 162 | def test_flat_multisuite_mocking_calls(self): 163 | test_cloc_out_path = os.path.join(os.path.dirname(__file__), 'fixtures', 'cloc_out.json') 164 | cloc_out = read_file(test_cloc_out_path) 165 | root_path = join(tempfile.gettempdir(), 'multisuite_test') 166 | app_path = join(root_path, 'apps', 'mockapp') 167 | log_path = join(root_path, 'logs') 168 | args = [ 169 | "--log_dir", log_path, "--app_gen_output_dir", root_path, "--test_build_only", "--switch_xcode_versions", 170 | "--project_generator_type", "bazel", 171 | "--full_clean" 172 | ] 173 | 174 | # we need the unused named variable for mocking purposes 175 | # noinspection PyUnusedLocal 176 | def command_callable(command, stdin): 177 | if 'cloc' in command: 178 | return PopenBehaviour(stdout=cloc_out) 179 | elif 'xcodebuild -version' in command: 180 | return PopenBehaviour(stdout=b'Xcode 10.0\nBuild version 10A255\n') 181 | return PopenBehaviour(stdout=b'test_out', stderr=b'test_error') 182 | 183 | with testfixtures.Replacer() as rep: 184 | mock_popen = MockPopen() 185 | rep.replace('subprocess.Popen', mock_popen) 186 | mock_popen.set_default(behaviour=command_callable) 187 | 188 | with mock.patch('distutils.spawn.find_executable') as mock_find: 189 | mock_find.return_value = '/bin/ls' # A non empty return value basically means "I found that executable" 190 | CommandLineMultisuite().main(args) 191 | self.assertGreater(os.listdir(app_path), 0) 192 | self.verify_genproj(app_path, 104, 601, 0) 193 | self.verify_lib(app_path, 'MockLib53') 194 | 195 | @integration_test 196 | def test_dot_multisuite(self): 197 | test_fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures', 'test_dot.gv') 198 | root_path = join(tempfile.gettempdir(), 'multisuite_test') 199 | app_path = join(root_path, 'apps', 'mockapp') 200 | log_path = join(root_path, 'logs') 201 | args = [ 202 | "--log_dir", log_path, "--app_gen_output_dir", root_path, "--dot_file", test_fixture_path, "--dot_root", 203 | "DotReaderMainModule", "--skip_xcode_build", "--test_build_only", 204 | "--project_generator_type", "bazel" 205 | ] 206 | command = CommandLineMultisuite() 207 | command.main(args) 208 | self.assertGreater(os.listdir(app_path), 0) 209 | self.verify_genproj(app_path, 341, 338, 0) 210 | self.verify_lib(app_path, 'DotReaderLib17') 211 | 212 | @integration_test 213 | def test_all_multisuite(self): 214 | test_fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures', 'test_dot.gv') 215 | root_path = join(tempfile.gettempdir(), 'multisuite_test') 216 | app_path = join(root_path, 'apps', 'mockapp') 217 | log_path = join(root_path, 'logs') 218 | args = [ 219 | "--log_dir", log_path, "--app_gen_output_dir", root_path, "--dot_file", test_fixture_path, "--dot_root", 220 | "DotReaderMainModule", "--skip_xcode_build", "--swift_lines_of_code", "150000", 221 | "--project_generator_type", "bazel" 222 | ] 223 | command = CommandLineMultisuite() 224 | command.main(args) 225 | self.assertGreater(os.listdir(app_path), 0) 226 | 227 | # Note we are assuming that the last project to be generated is the dot project. 228 | # If you change the order of project generation, make this match whatever is the new 'last project' 229 | # It's a bit fragile, but it's better than not verifying anything currently 230 | self.verify_genproj(app_path, 341, 675, 0) 231 | self.verify_lib(app_path, 'DotReaderLib17') 232 | -------------------------------------------------------------------------------- /uberpoet/blazeprojectgen.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Uber Technologies, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from __future__ import absolute_import 16 | 17 | import json 18 | import math 19 | import shutil 20 | from os.path import basename, dirname, join 21 | 22 | from . import locreader 23 | from .filegen import Language, ObjCHeaderFileGenerator, ObjCSourceFileGenerator, SwiftFileGenerator 24 | from .loccalc import LOCCalculator 25 | from .moduletree import ModuleNode 26 | from .util import first_in_dict, first_key, makedir 27 | 28 | 29 | class BlazeProjectGenerator(object): 30 | DIR_NAME = dirname(__file__) 31 | RESOURCE_DIR = join(DIR_NAME, "resources") 32 | 33 | def __init__(self, app_root, blaze_app_root, use_wmo=False, flavor='buck'): 34 | self.app_root = app_root 35 | self.blaze_app_root = blaze_app_root 36 | self.bzl_lib_template = self.load_resource("mock{}libtemplate.bzl".format(flavor)) 37 | self.bzl_app_template = self.load_resource("mock{}apptemplate.bzl".format(flavor)) 38 | self.app_delegate_template = self.load_resource("mockappdelegate") 39 | self.swift_gen = SwiftFileGenerator() 40 | self.objc_source_gen = ObjCSourceFileGenerator() 41 | self.objc_header_gen = ObjCHeaderFileGenerator() 42 | self.loc_calc = LOCCalculator() 43 | self.use_wmo = use_wmo 44 | self.flavor = flavor 45 | self.swift_file_size_loc = self.loc_calc.calculate_loc( 46 | self.swift_gen.gen_file(3, 3).text, self.swift_gen.language()) 47 | self.objc_file_size_loc = self.loc_calc.calculate_loc( 48 | self.objc_source_gen.gen_file(3, 3).text, self.objc_source_gen.language()) 49 | 50 | @property 51 | def wmo_state(self): 52 | return "YES" if self.use_wmo else "NO" 53 | 54 | @staticmethod 55 | def load_resource(name): 56 | with open(join(BlazeProjectGenerator.RESOURCE_DIR, name), "r") as f: 57 | return f.read() 58 | 59 | @staticmethod 60 | def copy_resource(name, dest): 61 | origin = join(BlazeProjectGenerator.RESOURCE_DIR, name) 62 | shutil.copyfile(origin, dest) 63 | 64 | @staticmethod 65 | def copy_resource_dir(name, dest): 66 | origin = join(BlazeProjectGenerator.RESOURCE_DIR, name) 67 | shutil.copytree(origin, dest) 68 | 69 | @staticmethod 70 | def write_file(path, text): 71 | with open(path, "w") as f: 72 | f.write(text) 73 | 74 | @staticmethod 75 | def make_list_str(items): 76 | return (",\n" + (" " * 8)).join(items) 77 | 78 | def make_dep_list(self, items): 79 | return self.make_list_str(["'//{0}:{0}'".format(i) for i in items]) 80 | 81 | def make_scheme_list(self, items): 82 | return self.make_list_str( 83 | ["{2: <20} :'/{0}/{1}:{1}Scheme'".format(self.blaze_app_root, i, "'{}'".format(i)) for i in items]) 84 | 85 | def example_command(self): 86 | if self.flavor == 'buck': 87 | return "buck project //App:App" 88 | elif self.flavor == 'bazel': 89 | return "Use Tulsi or XCHammer to generate an Xcode project." 90 | 91 | # Generation Functions 92 | 93 | def gen_app(self, app_node, node_list, target_swift_loc, target_objc_loc, loc_json_file_path): 94 | library_node_list = [n for n in node_list if n.node_type == ModuleNode.LIBRARY] 95 | 96 | if loc_json_file_path: 97 | loc_reader = locreader.LocFileReader() 98 | loc_reader.read_loc_file(loc_json_file_path) 99 | module_index = {} 100 | for n in library_node_list: 101 | loc = loc_reader.loc_for_module(n.name) 102 | language = loc_reader.language_for_module(n.name) 103 | module_index[n.name] = { 104 | "files": self.gen_lib_module(module_index, n, loc, language), 105 | "loc": loc, 106 | "language": language 107 | } 108 | else: 109 | total_code_units = 0 110 | for l in library_node_list: 111 | total_code_units += l.code_units 112 | 113 | total_loc = target_swift_loc + target_objc_loc 114 | swift_module_count_percentage = round(float(target_swift_loc) / total_loc, 2) 115 | loc_per_unit = total_loc / total_code_units 116 | 117 | module_index = {} 118 | max_swift_index = int(math.ceil((len(library_node_list) * swift_module_count_percentage))) 119 | for idx, n in enumerate(library_node_list): 120 | language = Language.OBJC if idx >= max_swift_index else Language.SWIFT 121 | module_index[n.name] = { 122 | "files": self.gen_lib_module(module_index, n, loc_per_unit, language), 123 | "loc": loc_per_unit, 124 | "language": language 125 | } 126 | 127 | app_module_dir = join(self.app_root, "App") 128 | makedir(app_module_dir) 129 | 130 | app_build_file = "BUCK" if self.flavor == 'buck' else "BUILD" 131 | app_files = { 132 | "AppDelegate.swift": self.gen_app_main(app_node, module_index), 133 | app_build_file: self.gen_app_build(app_node, library_node_list), 134 | } 135 | 136 | self.copy_resource("Info.plist", join(app_module_dir, "Info.plist")) 137 | 138 | if self.flavor == 'buck': 139 | self.copy_resource("mockbuckconfig", join(self.app_root, ".buckconfig")) 140 | elif self.flavor == 'bazel': 141 | self.copy_resource("mockbazelworkspace", join(self.app_root, "WORKSPACE")) 142 | self.copy_resource_dir("tools", join(self.app_root, "tools")) 143 | 144 | for name, text in app_files.iteritems(): 145 | self.write_file(join(app_module_dir, name), text) 146 | 147 | if loc_json_file_path: 148 | # Copy the LOC file into the generated project. 149 | shutil.copyfile(loc_json_file_path, join(self.app_root, basename(loc_json_file_path))) 150 | 151 | serializable_module_index = { 152 | key: { 153 | "file_count": len(value["files"]), 154 | "loc": value["loc"] 155 | } for key, value in module_index.items() 156 | } 157 | 158 | with open(join(self.app_root, "module_index.json"), "w") as module_index_json_file: 159 | json.dump(serializable_module_index, module_index_json_file) 160 | 161 | def gen_app_build(self, node, all_nodes): 162 | module_dep_list = self.make_dep_list([i.name for i in node.deps]) 163 | module_scheme_list = self.make_scheme_list([i.name for i in all_nodes]) 164 | return self.bzl_app_template.format(module_scheme_list, module_dep_list, self.wmo_state) 165 | 166 | def gen_app_main(self, app_node, module_index): 167 | importing_module_name = app_node.deps[0].name 168 | file_index = first_in_dict(module_index[importing_module_name]["files"]) 169 | language = module_index[importing_module_name]["language"] 170 | class_key = first_key(file_index.classes) 171 | class_index = first_in_dict(file_index.classes) 172 | function_key = first_in_dict(class_index)[0] 173 | return self.swift_gen.gen_main(self.app_delegate_template, importing_module_name, class_key, function_key, 174 | language) 175 | 176 | # Library Generation 177 | 178 | def gen_lib_module(self, module_index, module_node, loc_per_unit, language): 179 | deps = self.make_dep_list([i.name for i in module_node.deps]) 180 | build_text = self.bzl_lib_template.format(module_node.name, deps, self.wmo_state) 181 | # We now return a topologically sorted list of the graph which means that we will already have the 182 | # deps of a module inside the module index before we process this one. This allows us to reach into 183 | # the generated sources for the dependencies in order to create an instance of their class and 184 | # invoke their functions. 185 | deps_from_index = [{n.name: module_index[n.name]} for n in module_node.deps] 186 | 187 | # Make Text 188 | if language == Language.SWIFT: 189 | file_count = ( 190 | max(self.swift_file_size_loc, loc_per_unit) * module_node.code_units) / self.swift_file_size_loc 191 | if file_count < 1: 192 | raise ValueError( 193 | "Lines of code count is too small for the module {} to fit one file, increase it.".format( 194 | module_node.name)) 195 | files = { 196 | "File{}.swift".format(i): self.swift_gen.gen_file(3, 3, deps_from_index) for i in xrange(file_count) 197 | } 198 | elif language == Language.OBJC: 199 | file_count = (max(self.objc_file_size_loc, loc_per_unit) * module_node.code_units) / self.objc_file_size_loc 200 | if file_count < 1: 201 | raise ValueError( 202 | "Lines of code count is too small for the module {} to fit one file, increase it.".format( 203 | module_node.name)) 204 | files = {} 205 | for i in xrange(file_count): 206 | objc_source_file = self.objc_source_gen.gen_file( 207 | 3, 3, import_list=deps_from_index + ['File{}.h'.format(i)]) 208 | files["File{}.m".format(i)] = objc_source_file 209 | files["File{}.h".format(i)] = self.objc_header_gen.gen_file(objc_source_file) 210 | 211 | # Make Module Directories 212 | module_dir_path = join(self.app_root, module_node.name) 213 | files_dir_path = join(module_dir_path, "Sources") 214 | makedir(module_dir_path) 215 | makedir(files_dir_path) 216 | 217 | # Write BUCK or BUILD Files 218 | build_name = "BUCK" if self.flavor == 'buck' else "BUILD" 219 | build_path = join(module_dir_path, build_name) 220 | self.write_file(build_path, build_text) 221 | 222 | # Write Swift Files 223 | for file_name, file_obj in files.iteritems(): 224 | file_path = join(files_dir_path, file_name) 225 | self.write_file(file_path, file_obj.text) 226 | file_obj.text = "" # Save memory after write 227 | 228 | module_node.extra_info = files 229 | 230 | return files 231 | -------------------------------------------------------------------------------- /uberpoet/cpprojectgen.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Uber Technologies, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from __future__ import absolute_import 16 | 17 | import json 18 | import math 19 | import shutil 20 | from os.path import basename, dirname, join 21 | 22 | from . import locreader 23 | from .filegen import Language, ObjCHeaderFileGenerator, ObjCSourceFileGenerator, SwiftFileGenerator 24 | from .loccalc import LOCCalculator 25 | from .moduletree import ModuleNode 26 | from .util import first_in_dict, first_key, makedir 27 | 28 | 29 | class CocoaPodsProjectGenerator(object): 30 | DIR_NAME = dirname(__file__) 31 | RESOURCE_DIR = join(DIR_NAME, "resources") 32 | 33 | def __init__(self, 34 | app_root, 35 | use_wmo=False, 36 | use_dynamic_linking=False, 37 | use_deterministic_uuids=True, 38 | generate_multiple_pod_projects=False): 39 | self.app_root = app_root 40 | self.pod_lib_template = self.load_resource("mockcplibtemplate.podspec") 41 | self.pod_app_template = self.load_resource("mockcpapptemplate.podspec") 42 | self.podfile_template = self.load_resource("mockpodfile") 43 | self.app_delegate_template = self.load_resource("mockappdelegate") 44 | self.swift_gen = SwiftFileGenerator() 45 | self.objc_source_gen = ObjCSourceFileGenerator() 46 | self.objc_header_gen = ObjCHeaderFileGenerator() 47 | self.loc_calc = LOCCalculator() 48 | self.use_wmo = use_wmo 49 | self.use_dynamic_linking = use_dynamic_linking 50 | self.use_deterministic_uuids = use_deterministic_uuids 51 | self.generate_multiple_pod_projects = generate_multiple_pod_projects 52 | self.swift_file_size_loc = self.loc_calc.calculate_loc( 53 | self.swift_gen.gen_file(3, 3).text, self.swift_gen.language()) 54 | self.objc_file_size_loc = self.loc_calc.calculate_loc( 55 | self.objc_source_gen.gen_file(3, 3).text, self.objc_source_gen.language()) 56 | 57 | @property 58 | def wmo_state(self): 59 | return "YES" if self.use_wmo else "NO" 60 | 61 | @staticmethod 62 | def load_resource(name): 63 | with open(join(CocoaPodsProjectGenerator.RESOURCE_DIR, name), "r") as f: 64 | return f.read() 65 | 66 | @staticmethod 67 | def copy_resource(name, dest): 68 | origin = join(CocoaPodsProjectGenerator.RESOURCE_DIR, name) 69 | shutil.copyfile(origin, dest) 70 | 71 | @staticmethod 72 | def write_file(path, text): 73 | with open(path, "w") as f: 74 | f.write(text) 75 | 76 | @staticmethod 77 | def make_list_str(items, padding=8): 78 | return ("\n" + (" " * padding)).join(items) 79 | 80 | def make_dep_list(self, items): 81 | return self.make_list_str(["s.dependency '{0}'".format(i) for i in items]) 82 | 83 | def make_podfile_dep_list(self, items): 84 | return self.make_list_str(["pod '{0}', :path => '{0}/{0}.podspec'".format(i) for i in items], 4) 85 | 86 | def example_command(self): 87 | return "pod install" 88 | 89 | # Generation Functions 90 | 91 | def gen_app(self, app_node, node_list, target_swift_loc, target_objc_loc, loc_json_file_path): 92 | library_node_list = [n for n in node_list if n.node_type == ModuleNode.LIBRARY] 93 | 94 | if loc_json_file_path: 95 | loc_reader = locreader.LocFileReader() 96 | loc_reader.read_loc_file(loc_json_file_path) 97 | module_index = {} 98 | for n in library_node_list: 99 | loc = loc_reader.loc_for_module(n.name) 100 | language = loc_reader.language_for_module(n.name) 101 | module_index[n.name] = { 102 | "files": self.gen_lib_module(module_index, n, loc, language), 103 | "loc": loc, 104 | "language": language 105 | } 106 | else: 107 | total_code_units = 0 108 | for l in library_node_list: 109 | total_code_units += l.code_units 110 | 111 | total_loc = target_swift_loc + target_objc_loc 112 | swift_module_count_percentage = round(float(target_swift_loc) / total_loc, 2) 113 | loc_per_unit = total_loc / total_code_units 114 | 115 | module_index = {} 116 | max_swift_index = int(math.ceil((len(library_node_list) * swift_module_count_percentage))) 117 | for idx, n in enumerate(library_node_list): 118 | language = Language.OBJC if idx >= max_swift_index else Language.SWIFT 119 | module_index[n.name] = { 120 | "files": self.gen_lib_module(module_index, n, loc_per_unit, language), 121 | "loc": loc_per_unit, 122 | "language": language 123 | } 124 | 125 | app_module_dir = join(self.app_root, "App") 126 | makedir(app_module_dir) 127 | 128 | app_files = { 129 | "AppDelegate.swift": self.gen_app_main(app_node, module_index), 130 | "dummy.swift": "", 131 | "AppContainer.podspec": self.gen_app_podspec(app_node), 132 | } 133 | 134 | self.copy_resource("Info.plist", join(app_module_dir, "Info.plist")) 135 | 136 | for name, text in app_files.iteritems(): 137 | self.write_file(join(app_module_dir, name), text) 138 | 139 | if loc_json_file_path: 140 | # Copy the LOC file into the generated project. 141 | shutil.copyfile(loc_json_file_path, join(self.app_root, basename(loc_json_file_path))) 142 | 143 | podfile_text = self.gen_podfile(library_node_list) 144 | podfile_path = join(self.app_root, "Podfile") 145 | self.write_file(podfile_path, podfile_text) 146 | 147 | serializable_module_index = { 148 | key: { 149 | "file_count": len(value["files"]), 150 | "loc": value["loc"] 151 | } for key, value in module_index.items() 152 | } 153 | 154 | with open(join(self.app_root, "module_index.json"), "w") as module_index_json_file: 155 | json.dump(serializable_module_index, module_index_json_file) 156 | 157 | def gen_app_podspec(self, node): 158 | module_dep_list = self.make_dep_list([i.name for i in node.deps]) 159 | return self.pod_app_template.format(module_dep_list, self.wmo_state) 160 | 161 | def gen_app_main(self, app_node, module_index): 162 | importing_module_name = app_node.deps[0].name 163 | file_index = first_in_dict(module_index[importing_module_name]["files"]) 164 | language = module_index[importing_module_name]["language"] 165 | class_key = first_key(file_index.classes) 166 | class_index = first_in_dict(file_index.classes) 167 | function_key = first_in_dict(class_index)[0] 168 | return self.swift_gen.gen_main(self.app_delegate_template, importing_module_name, class_key, function_key, 169 | language) 170 | 171 | # Library Generation 172 | 173 | def gen_lib_module(self, module_index, module_node, loc_per_unit, language): 174 | # Make Podspec Text 175 | deps = self.make_dep_list([i.name for i in module_node.deps]) 176 | pod_text = self.pod_lib_template.format(module_node.name, deps, self.wmo_state) 177 | # We now return a topologically sorted list of the graph which means that we will already have the 178 | # deps of a module inside the module index before we process this one. This allows us to reach into 179 | # the generated sources for the dependencies in order to create an instance of their class and 180 | # invoke their functions. 181 | deps_from_index = [{n.name: module_index[n.name]} for n in module_node.deps] 182 | 183 | # Make Text 184 | if language == Language.SWIFT: 185 | file_count = ( 186 | max(self.swift_file_size_loc, loc_per_unit) * module_node.code_units) / self.swift_file_size_loc 187 | if file_count < 1: 188 | raise ValueError( 189 | "Lines of code count is too small for the module {} to fit one file, increase it.".format( 190 | module_node.name)) 191 | files = { 192 | "File{}.swift".format(i): self.swift_gen.gen_file(3, 3, deps_from_index) for i in xrange(file_count) 193 | } 194 | elif language == Language.OBJC: 195 | file_count = (max(self.objc_file_size_loc, loc_per_unit) * module_node.code_units) / self.objc_file_size_loc 196 | if file_count < 1: 197 | raise ValueError( 198 | "Lines of code count is too small for the module {} to fit one file, increase it.".format( 199 | module_node.name)) 200 | files = {} 201 | for i in xrange(file_count): 202 | objc_source_file = self.objc_source_gen.gen_file( 203 | 3, 3, import_list=deps_from_index + ['File{}.h'.format(i)]) 204 | files["File{}.m".format(i)] = objc_source_file 205 | files["File{}.h".format(i)] = self.objc_header_gen.gen_file(objc_source_file) 206 | 207 | # Make Module Directories 208 | module_dir_path = join(self.app_root, module_node.name) 209 | files_dir_path = join(module_dir_path, "Sources") 210 | makedir(module_dir_path) 211 | makedir(files_dir_path) 212 | 213 | # Write podspec File 214 | pod_path = join(module_dir_path, "{0}.podspec".format(module_node.name)) 215 | self.write_file(pod_path, pod_text) 216 | 217 | # Write Swift Files 218 | for file_name, file_obj in files.iteritems(): 219 | file_path = join(files_dir_path, file_name) 220 | self.write_file(file_path, file_obj.text) 221 | file_obj.text = "" # Save memory after write 222 | 223 | module_node.extra_info = files 224 | 225 | return files 226 | 227 | # Podfile Generation 228 | 229 | def gen_podfile(self, all_nodes): 230 | podfile_module_dep_list = self.make_podfile_dep_list([i.name for i in all_nodes]) 231 | link_style = ":dynamic" if self.use_dynamic_linking else ":static" 232 | use_deterministic_uuids = str(self.use_deterministic_uuids).lower() 233 | generate_multiple_pod_projects = str(self.generate_multiple_pod_projects).lower() 234 | 235 | return self.podfile_template.format( 236 | "pod 'AppContainer', :path => 'App/AppContainer.podspec', :appspecs => ['App']", podfile_module_dep_list, 237 | link_style, use_deterministic_uuids, generate_multiple_pod_projects) 238 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2018 Uber Technologies Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /uberpoet/filegen.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Uber Technologies, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from __future__ import absolute_import 16 | 17 | from .util import first_in_dict, seed 18 | 19 | uber_poet_header = """ 20 | // This code was @generated by Uber Poet, a mock application generator. 21 | // Check it out at https://github.com/uber/uber-poet 22 | """ 23 | 24 | # This method can only be invoked between Swift modules as it is not 25 | # ObjC friendly due to the use of generics. 26 | swift_func_template = """ 27 | public func complexCrap{0}(arg: Int, stuff:T) -> Int {{ 28 | let a = Int(4 * {1} + Int(Float(arg) / 32.0)) 29 | let b = Int(4 * {1} + Int(Float(arg) / 32.0)) 30 | let c = Int(4 * {1} + Int(Float(arg) / 32.0)) 31 | return Int(4 * {1} + Int(Float(arg) / 32.0)) + a + b + c 32 | }}""" 33 | 34 | swift_func_objc_friendly_template = """ 35 | @objc 36 | public func complexStuff{0}(arg: String) -> String {{ 37 | let randomString = NSUUID().uuidString 38 | return ("\\(arg)-\\(randomString)") 39 | }} 40 | """ 41 | 42 | swift_class_template = """ 43 | @objc 44 | public class MyClass{0}: NSObject {{ 45 | public let x: Int 46 | public let y: String 47 | 48 | @objc 49 | public override init() {{ 50 | x = 7 51 | y = "hi" 52 | {2} 53 | }} 54 | 55 | {1} 56 | }}""" 57 | 58 | swift_to_swift_func_call_template = """MyClass{0}().complexCrap{1}(arg: 4, stuff: 2)""" 59 | swift_to_objc_func_call_template = """MyClass_{0}().complexCrap{1}(4, stuff: \"2\")""" 60 | 61 | swift_to_swift_objc_friendly_func_call_template = """MyClass{0}().complexStuff{1}(arg: \"4\")""" 62 | swift_to_objc_friendly_func_call_template = """MyClass_{0}().complexStuff{1}(arg: \"4\")""" 63 | 64 | objc_to_swift_func_call_template = """[[[MyClass{} alloc] init] complexStuff{}WithArg:@\"4\"];""" 65 | objc_to_objc_func_call_template = """[[[MyClass_{} alloc] init] complexCrap{}:4 stuff:@\"2\"];""" 66 | 67 | objc_header_func_template = """- (int)complexCrap{0}:(int)arg stuff:(nonnull NSString *)stuff;""" 68 | 69 | objc_source_func_template = """ 70 | - (int)complexCrap{0}:(int)arg stuff:(nonnull NSString *)stuff; 71 | {{ 72 | int a = (int)(4 * self.{1} + (int)((float)arg / 32.0)); 73 | int b = (int)(4 * self.{1} + (int)((float)arg / 32.0)); 74 | int c = (int)(4 * self.{1} + (int)((float)arg / 32.0)); 75 | return (int)(4 * self.{1} + (int)((float)arg / 32.0)) + a + b + c; 76 | }}""" 77 | 78 | objc_system_import_template = """ 79 | @import Foundation; 80 | """ 81 | 82 | objc_header_template = """ 83 | @interface MyClass_{0} : NSObject 84 | 85 | @property(nonatomic, readonly) int x; 86 | @property(nonatomic, readonly, nonnull) NSString *y; 87 | 88 | - (nonnull instancetype)initWithX:(int)x y:(nonnull NSString *)y; 89 | {1} 90 | 91 | @end 92 | """ 93 | 94 | objc_source_template = """ 95 | @implementation MyClass_{0} 96 | 97 | - (nonnull instancetype)initWithX:(int)x y:(nonnull NSString *)y; 98 | {{ 99 | self = [super init]; 100 | NSParameterAssert(self); 101 | 102 | _x = x; 103 | _y = y; 104 | {2} 105 | return self; 106 | }} 107 | {1} 108 | 109 | @end 110 | """ 111 | 112 | 113 | def get_func_call_template(from_language, to_language, function_type): 114 | if function_type == FuncType.SWIFT_ONLY: 115 | if from_language == Language.OBJC: 116 | raise ValueError("Cannot invoke SWIFT_ONLY method from ObjC!") 117 | return swift_to_swift_func_call_template if to_language == Language.SWIFT else swift_to_objc_func_call_template 118 | elif function_type == FuncType.OBJC_FRIENDLY: 119 | if from_language == Language.SWIFT: 120 | if to_language == Language.SWIFT: 121 | return swift_to_swift_objc_friendly_func_call_template 122 | elif to_language == Language.OBJC: 123 | return swift_to_objc_friendly_func_call_template 124 | elif from_language == Language.OBJC: 125 | if to_language == Language.SWIFT: 126 | return objc_to_swift_func_call_template 127 | elif to_language == Language.OBJC: 128 | return objc_to_objc_func_call_template 129 | 130 | 131 | def get_import_func_calls(from_language, import_list, indent=0): 132 | out = [] 133 | for i in import_list: 134 | if type(i) is str: 135 | continue 136 | module = first_in_dict(i) 137 | to_language = module["language"] 138 | for file_result in module["files"].values(): 139 | for class_num, class_funcs in file_result.classes.items(): 140 | for func_type, func_nums in class_funcs.items(): 141 | for func_num in func_nums: 142 | if (func_type == FuncType.SWIFT_ONLY and from_language == Language.OBJC and 143 | to_language == Language.SWIFT): 144 | # We cannot invoke Swift only functions from ObjC since they use generics. 145 | continue 146 | text = get_func_call_template(from_language, to_language, func_type).format(class_num, func_num) 147 | indented_text = '\n'.join(" " * indent + line for line in text.splitlines()) 148 | out.append(indented_text) 149 | 150 | return "\n".join(out) 151 | 152 | 153 | class Language(object): 154 | SWIFT = 'Swift' 155 | OBJC = 'Objective-C' 156 | 157 | @staticmethod 158 | def enum_list(): 159 | return [Language.SWIFT, Language.OBJC] 160 | 161 | 162 | class FuncType(object): 163 | """ 164 | Describes the type of function added to a class. Helps distinguish how to invoke a function 165 | between modules. 166 | """ 167 | SWIFT_ONLY = 'swift_only' 168 | OBJC_FRIENDLY = 'objc_friendly' 169 | 170 | 171 | class FileResult(object): 172 | 173 | def __init__(self, text, functions, classes): 174 | super(FileResult, self).__init__() 175 | self.text = text # string 176 | self.text_line_count = len(text.split('\n')) 177 | self.functions = functions # list of indexes 178 | self.classes = classes # {class index: {func type: function indexes}} 179 | 180 | def __str__(self): 181 | return "".format(self.text_line_count, self.functions, 182 | self.classes) 183 | 184 | 185 | class FileGenerator(object): 186 | 187 | def gen_file(self, class_count, function_count): 188 | return FileResult("", [], {}) 189 | 190 | 191 | class ObjCHeaderFileGenerator(FileGenerator): 192 | 193 | @staticmethod 194 | def language(): 195 | return Language.OBJC 196 | 197 | @staticmethod 198 | def extension(): 199 | return '.h' 200 | 201 | @staticmethod 202 | def gen_func(nums): 203 | out = [] 204 | 205 | for num in nums: 206 | out.append(objc_header_func_template.format(num)) 207 | 208 | return "\n".join(out), nums 209 | 210 | def get_header(self, objc_class): 211 | out = [] 212 | class_nums = {} 213 | 214 | for c in objc_class.classes: 215 | num = c 216 | func_out, func_nums = self.gen_func(objc_class.classes[c][FuncType.OBJC_FRIENDLY]) 217 | out.append(objc_header_template.format(num, func_out)) 218 | class_nums[num] = {FuncType.OBJC_FRIENDLY: func_nums} 219 | 220 | return "\n".join(out), class_nums 221 | 222 | def gen_file(self, objc_class): 223 | class_out, class_nums = self.get_header(objc_class) 224 | 225 | chunks = [uber_poet_header, objc_system_import_template, class_out] 226 | 227 | return FileResult("\n".join(chunks), [], class_nums) 228 | 229 | 230 | class ObjCSourceFileGenerator(FileGenerator): 231 | 232 | @staticmethod 233 | def language(): 234 | return Language.OBJC 235 | 236 | @staticmethod 237 | def extension(): 238 | return '.m' 239 | 240 | @staticmethod 241 | def gen_func(function_count, var_name): 242 | out = [] 243 | nums = [] 244 | 245 | for _ in xrange(function_count): 246 | num = seed() 247 | text = objc_source_func_template.format(num, var_name) 248 | nums.append(num) 249 | out.append(text) 250 | 251 | return "\n".join(out), nums 252 | 253 | def gen_class(self, class_count, func_per_class_count, import_list): 254 | out = [] 255 | class_nums = {} 256 | 257 | for _ in xrange(class_count): 258 | num = seed() 259 | func_out, func_nums = self.gen_func(func_per_class_count, "x") 260 | func_call_out = get_import_func_calls(self.language(), import_list, indent=4) 261 | out.append(objc_source_template.format(num, func_out, func_call_out)) 262 | class_nums[num] = {FuncType.OBJC_FRIENDLY: func_nums} 263 | 264 | return "\n".join(out), class_nums 265 | 266 | def gen_file(self, class_count, function_count, import_list=None): 267 | if import_list is None: 268 | import_list = [] 269 | imports = [] 270 | for i in import_list: 271 | if type(i) is str: 272 | imports.append('#import \"{}\"'.format(i)) 273 | elif type(i) is dict: 274 | imports.append('@import {};'.format(i.keys()[0])) 275 | imports_out = "\n".join(imports) 276 | class_out, class_nums = self.gen_class(class_count, 5, import_list) 277 | 278 | chunks = [uber_poet_header, objc_system_import_template, imports_out, class_out] 279 | 280 | return FileResult("\n".join(chunks), [], class_nums) 281 | 282 | 283 | class SwiftFileGenerator(FileGenerator): 284 | 285 | def __init__(self): 286 | self.gen_state = {} 287 | 288 | @staticmethod 289 | def language(): 290 | return Language.SWIFT 291 | 292 | @staticmethod 293 | def extension(): 294 | return '.swift' 295 | 296 | @staticmethod 297 | def gen_func(function_count, var_name, indent=0): 298 | out = [] 299 | nums = [] 300 | 301 | for _ in xrange(function_count): 302 | num = seed() 303 | text = swift_func_template.format(num, var_name) 304 | indented_text = '\n'.join(" " * indent + line for line in text.splitlines()) 305 | nums.append(num) 306 | out.append(indented_text) 307 | 308 | return "\n".join(out), nums 309 | 310 | @staticmethod 311 | def gen_objc_friendly_func(indent=0): 312 | out = [] 313 | nums = [] 314 | 315 | num = seed() 316 | text = swift_func_objc_friendly_template.format(num) 317 | indented_text = '\n'.join(" " * indent + line for line in text.splitlines()) 318 | nums.append(num) 319 | out.append(indented_text) 320 | 321 | return "\n".join(out), nums 322 | 323 | def gen_class(self, class_count, func_per_class_count, import_list): 324 | out = [] 325 | class_nums = {} 326 | 327 | for _ in xrange(class_count): 328 | num = seed() 329 | swift_only_func_out, swift_only_func_nums = self.gen_func(func_per_class_count, "x", indent=4) 330 | swift_objc_friendly_func_out, swift_objc_friendly_func_nums = self.gen_objc_friendly_func(indent=4) 331 | func_out = swift_only_func_out + "\n" + swift_objc_friendly_func_out 332 | func_call_out = get_import_func_calls(self.language(), import_list, indent=8) 333 | out.append(swift_class_template.format(num, func_out, func_call_out)) 334 | 335 | class_nums[num] = { 336 | FuncType.SWIFT_ONLY: swift_only_func_nums, 337 | FuncType.OBJC_FRIENDLY: swift_objc_friendly_func_nums 338 | } 339 | 340 | return "\n".join(out), class_nums 341 | 342 | def gen_file(self, class_count, function_count, import_list=None): 343 | if import_list is None: 344 | import_list = [] 345 | imports_out = "\n".join(["import {}".format(i if type(i) is str else i.keys()[0]) for i in import_list]) 346 | func_out, func_nums = self.gen_func(function_count, "7") 347 | class_out, class_nums = self.gen_class(class_count, 5, import_list) 348 | 349 | chunks = [uber_poet_header, imports_out, func_out, class_out] 350 | 351 | return FileResult("\n".join(chunks), func_nums, class_nums) 352 | 353 | @staticmethod 354 | def gen_main(template, importing_module_name, class_num, func_num, to_language): 355 | import_line = 'import {}'.format(importing_module_name) 356 | action_expr = get_func_call_template(Language.SWIFT, to_language, FuncType.SWIFT_ONLY).format( 357 | class_num, func_num) 358 | print_line = 'print("\\({})")'.format(action_expr) 359 | return template.format(uber_poet_header, import_line, print_line) 360 | --------------------------------------------------------------------------------