├── LICENSE ├── README.rst ├── py3_ready ├── __init__.py ├── apt_tracer.py ├── cli.py ├── dependency_tracer.py ├── dot.py ├── package_xml.py └── rosdep.py └── setup.py /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | py3-ready 3 | ========= 4 | 5 | This is a tool for checking if your ROS package or its dependencies depend on python 2. 6 | 7 | Install 8 | ^^^^^^^ 9 | 10 | This package works on Ubuntu and Debian, and it needs some packages installed on the system. 11 | 12 | Install these if the default ``python`` is Python 2 (Ubuntu Bionic, Debian Stretch, etc). 13 | 14 | :: 15 | 16 | $ sudo apt-get install python-apt 17 | $ sudo apt-get install python-rosdep-modules 18 | $ sudo apt-get install python-catkin-pkg-modules 19 | 20 | Install these if the default ``python`` is Python 3 (Ubuntu focal, Debian Buster, etc). 21 | 22 | :: 23 | 24 | $ sudo apt-get install python3-apt 25 | $ sudo apt-get install python3-rosdep-modules 26 | $ sudo apt-get install python3-catkin-pkg-modules 27 | 28 | 29 | Then install from PyPI.org. 30 | 31 | :: 32 | 33 | $ pip install py3-ready 34 | 35 | If you would like to install from source then create a virtual environment with access to system packages. 36 | 37 | :: 38 | 39 | $ cd py3-ready/ 40 | # Set up Python 2 virtual environment 41 | $ virtualenv --system-site-packages ssenv2 42 | $ . ssenv2/bin/activate 43 | $ python setup.py develop 44 | $ deactivate 45 | # Set up Python 3 virtual environment 46 | $ python3 venv --system-site-packages ssenv3 47 | $ . ssenv3/bin/activate 48 | $ python setup.py develop 49 | 50 | 51 | Usage 52 | ^^^^^ 53 | All commands exit with code 1 if the package does depend on python 2, and 0 if does not. 54 | If any unrecoverable error occurs then the exit code is 2. 55 | 56 | check-package 57 | ::::::::::::::::: 58 | 59 | This checks if any dependency of a ROS package depends on python2. 60 | The command takes a name of a ROS package. 61 | The package must exist in a sourced workspace. 62 | Use **--quiet** to suppress warnings and human readable output. 63 | 64 | :: 65 | 66 | $ py3-ready check-package catkin 67 | python-argparse did not resolve to an apt package 68 | /opt/ros/melodic/share/catkin depends on python 69 | 70 | Passing **--dot** outputs the dependency graph in `DOT `_ format. 71 | 72 | :: 73 | 74 | $ py3-ready check-package --quiet --dot catkin 75 | digraph G { 76 | "catkin%package" -> "python-empy%rosdep"[color=pink]; // build_export_depend 77 | "python-empy%rosdep" -> "python-empy%apt"[color=orange]; // rosdep 78 | "python-catkin-pkg%rosdep" -> "python-catkin-pkg%apt"[color=orange]; // rosdep 79 | "catkin%package" -> "python-mock%rosdep"[color=pink]; // test_depend 80 | "python-mock%apt" -> "python-funcsigs%apt"[color=blue]; // Depends 81 | "python:any%apt" -> "python%apt"[color=green]; // virtual 82 | "python-pbr%apt" -> "python-six%apt"[color=blue]; // Depends 83 | "google-mock%apt" -> "googletest%apt"[color=blue]; // Depends 84 | "python-mock%apt" -> "python-pbr%apt"[color=blue]; // Depends 85 | "python-pbr%apt" -> "python-pkg-resources%apt"[color=blue]; // Depends 86 | "python-nose%apt" -> "python-pkg-resources%apt"[color=blue]; // Depends 87 | "catkin%package" -> "python-empy%rosdep"[color=pink]; // build_depend 88 | "python-docutils%apt" -> "python:any%apt"[color=blue]; // Depends 89 | "catkin%package" -> "python-catkin-pkg%rosdep"[color=pink]; // build_export_depend 90 | "python-dateutil%apt" -> "python:any%apt"[color=blue]; // Depends 91 | "python-nose%apt" -> "python:any%apt"[color=blue]; // Depends 92 | "python-funcsigs%apt" -> "python:any%apt"[color=blue]; // Depends 93 | "gtest%rosdep" -> "libgtest-dev%apt"[color=orange]; // rosdep 94 | "python-catkin-pkg%apt" -> "python-docutils%apt"[color=blue]; // Depends 95 | "catkin%package" -> "python-catkin-pkg%rosdep"[color=pink]; // build_depend 96 | "python-docutils%apt" -> "python-roman%apt"[color=blue]; // Depends 97 | "python-mock%rosdep" -> "python-mock%apt"[color=orange]; // rosdep 98 | "python-nose%rosdep" -> "python-nose%apt"[color=orange]; // rosdep 99 | "google-mock%rosdep" -> "google-mock%apt"[color=orange]; // rosdep 100 | "catkin%package" -> "python-catkin-pkg%rosdep"[color=pink]; // exec_depend 101 | "python-catkin-pkg-modules%apt" -> "python-pyparsing%apt"[color=blue]; // Depends 102 | "catkin%package" -> "gtest%rosdep"[color=pink]; // build_export_depend 103 | "catkin%package" -> "python-nose%rosdep"[color=pink]; // build_export_depend 104 | "python-six%apt" -> "python:any%apt"[color=blue]; // Depends 105 | "python-dateutil%apt" -> "python-six%apt"[color=blue]; // Depends 106 | "python-catkin-pkg%apt" -> "python-pyparsing%apt"[color=blue]; // Depends 107 | "python-catkin-pkg-modules%apt" -> "python-docutils%apt"[color=blue]; // Depends 108 | "python-pbr%apt" -> "python:any%apt"[color=blue]; // Depends 109 | "python-pyparsing%apt" -> "python:any%apt"[color=blue]; // Depends 110 | "python-catkin-pkg%apt" -> "python:any%apt"[color=blue]; // Depends 111 | "python-catkin-pkg-modules%apt" -> "python:any%apt"[color=blue]; // Depends 112 | "python-mock%apt" -> "python-six%apt"[color=blue]; // Depends 113 | "catkin%package" -> "python-nose%rosdep"[color=pink]; // test_depend 114 | "python-empy%apt" -> "python%apt"[color=blue]; // Depends 115 | "python-mock%apt" -> "python:any%apt"[color=blue]; // Depends 116 | "python-catkin-pkg%apt" -> "python-dateutil%apt"[color=blue]; // Depends 117 | "python-catkin-pkg%apt" -> "python-catkin-pkg-modules%apt"[color=blue]; // Depends 118 | "googletest%apt" -> "python:any%apt"[color=blue]; // Depends 119 | "python-empy%apt" -> "python:any%apt"[color=blue]; // Depends 120 | "catkin%package" -> "google-mock%rosdep"[color=pink]; // build_export_depend 121 | "python-catkin-pkg-modules%apt" -> "python-dateutil%apt"[color=blue]; // Depends 122 | "libgtest-dev%apt" -> "googletest%apt"[color=blue]; // Depends 123 | "python-pkg-resources%apt" -> "python:any%apt"[color=blue]; // Depends 124 | "python-roman%apt" -> "python:any%apt"[color=blue]; // Depends 125 | 126 | "python-mock%rosdep"[color=orange,shape=rect][label="python-mock"]; // rosdep 127 | "python-mock%apt"[label="python-mock"]; // apt 128 | "python-catkin-pkg-modules%apt"[label="python-catkin-pkg-modules"]; // apt 129 | "python-pyparsing%apt"[label="python-pyparsing"]; // apt 130 | "python-catkin-pkg%apt"[label="python-catkin-pkg"]; // apt 131 | "gtest%rosdep"[color=orange,shape=rect][label="gtest"]; // rosdep 132 | "python:any%apt"[label="python:any"]; // apt 133 | "python-dateutil%apt"[label="python-dateutil"]; // apt 134 | "python-roman%apt"[label="python-roman"]; // apt 135 | "catkin%package"[color=pink,shape=hexagon][label="catkin"]; // package 136 | "python-empy%apt"[label="python-empy"]; // apt 137 | "google-mock%apt"[label="google-mock"]; // apt 138 | "python-nose%rosdep"[color=orange,shape=rect][label="python-nose"]; // rosdep 139 | "python-pbr%apt"[label="python-pbr"]; // apt 140 | "python-pkg-resources%apt"[label="python-pkg-resources"]; // apt 141 | "python-funcsigs%apt"[label="python-funcsigs"]; // apt 142 | "python-nose%apt"[label="python-nose"]; // apt 143 | "python%apt"[label="python"]; // apt 144 | "google-mock%rosdep"[color=orange,shape=rect][label="google-mock"]; // rosdep 145 | "python-empy%rosdep"[color=orange,shape=rect][label="python-empy"]; // rosdep 146 | "python-catkin-pkg%rosdep"[color=orange,shape=rect][label="python-catkin-pkg"]; // rosdep 147 | "libgtest-dev%apt"[label="libgtest-dev"]; // apt 148 | "googletest%apt"[label="googletest"]; // apt 149 | "python-docutils%apt"[label="python-docutils"]; // apt 150 | "python-six%apt"[label="python-six"]; // apt 151 | } 152 | 153 | By default this looks for dependencies on the debian package named **python**. 154 | Use **--target** to change this name. 155 | 156 | :: 157 | 158 | $ py3-ready check-package --target python3 gazebo_ros 2>/dev/null 159 | /opt/ros/melodic/share/gazebo_ros depends on python3 160 | 161 | check-rosdep 162 | :::::::::::: 163 | 164 | This uses **rosdep** and **apt** to check if a rosdep key recursively depends on python 2. 165 | 166 | :: 167 | 168 | $ py3-ready check-rosdep python-sip 169 | rosdep key python-sip depends on python 170 | 171 | Passing **--dot** outputs the dependency graph in `DOT `_ format. 172 | Use **--quiet** to suppress warnings and human readable output. 173 | 174 | :: 175 | 176 | $ py3-ready check-rosdep --quiet --dot boost 177 | digraph G { 178 | "libboost-mpi-python1.65-dev%apt" -> "libboost-mpi-python1.65.1%apt"[color=blue]; // Depends 179 | "libboost-mpi-python1.65.1%apt" -> "python%apt"[color=blue]; // Depends 180 | "libboost-all-dev%apt" -> "libboost-mpi-python-dev%apt"[color=blue]; // Depends 181 | "libboost-mpi-python-dev%apt" -> "libboost-mpi-python1.65-dev%apt"[color=blue]; // Depends 182 | "libboost-python1.65-dev%apt" -> "python-dev%apt"[color=blue]; // Depends 183 | "libboost-mpi-python1.65.1%apt" -> "python:any%apt"[color=blue]; // Depends 184 | "python:any%apt" -> "python%apt"[color=green]; // virtual 185 | "libboost-python-dev%apt" -> "libboost-python1.65-dev%apt"[color=blue]; // Depends 186 | "boost%rosdep" -> "libboost-all-dev%apt"[color=orange]; // rosdep 187 | "python-dev%apt" -> "python%apt"[color=blue]; // Depends 188 | "libboost-all-dev%apt" -> "libboost-python-dev%apt"[color=blue]; // Depend 189 | "python%apt"[label="python"]; // apt 190 | 191 | "libboost-mpi-python-dev%apt"[label="libboost-mpi-python-dev"]; // apt 192 | "boost%rosdep"[color=orange,shape=rect][label="boost"]; // rosdep 193 | "libboost-python-dev%apt"[label="libboost-python-dev"]; // apt 194 | "libboost-mpi-python1.65-dev%apt"[label="libboost-mpi-python1.65-dev"]; // apt 195 | "libboost-python1.65-dev%apt"[label="libboost-python1.65-dev"]; // apt 196 | "libboost-mpi-python1.65.1%apt"[label="libboost-mpi-python1.65.1"]; // apt 197 | "python-dev%apt"[label="python-dev"]; // apt 198 | "python:any%apt"[label="python:any"]; // apt 199 | "libboost-all-dev%apt"[label="libboost-all-dev"]; // apt 200 | } 201 | 202 | 203 | By default this looks for dependencies on the debian package named **python**. 204 | Use **--target** to change this name. 205 | 206 | 207 | :: 208 | 209 | $ py3-ready check-rosdep --target python3 python-sip 210 | rosdep key python-sip does not depend on python3 211 | 212 | check-apt 213 | ::::::::: 214 | 215 | This uses **apt** to check if a debian package recursively depends on python 2. 216 | 217 | :: 218 | 219 | $ py3-ready check-apt libboost-python-dev 220 | libboost-python-dev depends on python 221 | 222 | 223 | Passing **--dot** outputs the dependency graph in `DOT `_ format. 224 | Use **--quiet** to suppress warnings and human readable output. 225 | 226 | :: 227 | 228 | $ py3-ready check-apt --dot --quiet libboost-all-dev 229 | digraph G { 230 | "libboost-mpi-python1.65.1%apt" -> "python:any%apt"[color=blue]; // Depends 231 | "libboost-all-dev%apt" -> "libboost-python-dev%apt"[color=blue]; // Depends 232 | "libboost-python-dev%apt" -> "libboost-python1.65-dev%apt"[color=blue]; // Depends 233 | "libboost-python1.65-dev%apt" -> "python-dev%apt"[color=blue]; // Depends 234 | "python-dev%apt" -> "python%apt"[color=blue]; // Depends 235 | "libboost-all-dev%apt" -> "libboost-mpi-python-dev%apt"[color=blue]; // Depends 236 | "libboost-mpi-python1.65-dev%apt" -> "libboost-mpi-python1.65.1%apt"[color=blue]; // Depends 237 | "libboost-mpi-python1.65.1%apt" -> "python%apt"[color=blue]; // Depends 238 | "python:any%apt" -> "python%apt"[color=green]; // virtual 239 | "libboost-mpi-python-dev%apt" -> "libboost-mpi-python1.65-dev%apt"[color=blue]; // Depends 240 | 241 | "libboost-python1.65-dev%apt"[label="libboost-python1.65-dev"]; // apt 242 | "python-dev%apt"[label="python-dev"]; // apt 243 | "python:any%apt"[label="python:any"]; // apt 244 | "python%apt"[label="python"]; // apt 245 | "libboost-mpi-python-dev%apt"[label="libboost-mpi-python-dev"]; // apt 246 | "libboost-mpi-python1.65-dev%apt"[label="libboost-mpi-python1.65-dev"]; // apt 247 | "libboost-python-dev%apt"[label="libboost-python-dev"]; // apt 248 | "libboost-all-dev%apt"[label="libboost-all-dev"]; // apt 249 | "libboost-mpi-python1.65.1%apt"[label="libboost-mpi-python1.65.1"]; // apt 250 | } 251 | 252 | 253 | By default this looks for dependencies on the debian package named **python**. 254 | Use **--target** to change this name. 255 | 256 | 257 | :: 258 | 259 | $ py3-ready check-apt --target python3 python3-apt 260 | python3-apt depends on python3 261 | -------------------------------------------------------------------------------- /py3_ready/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osrf/py3-ready/ae227460271e23f277da3155dc27793b8b753a0a/py3_ready/__init__.py -------------------------------------------------------------------------------- /py3_ready/apt_tracer.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Open Source Robotics Foundation, 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 | """Tools for walking through and tracing debian package dependencies.""" 16 | 17 | from __future__ import print_function 18 | 19 | import copy 20 | import sys 21 | 22 | from .dependency_tracer import DependencyTracer 23 | from .dependency_tracer import Edge 24 | from .dependency_tracer import Node 25 | from .dependency_tracer import TracerCache 26 | from .dot import paths_to_dot 27 | 28 | from apt.cache import Cache 29 | from apt.package import Package 30 | 31 | APT_NODE = 'apt' 32 | 33 | 34 | class AptTracer(DependencyTracer): 35 | 36 | def __init__(self, apt_cache=None, quiet=True): 37 | if not apt_cache: 38 | apt_cache = Cache() 39 | self._apt_cache = apt_cache 40 | self._quiet = quiet 41 | 42 | def trace_paths(self, start, target, cache=None): 43 | if start in self._apt_cache: 44 | start_pkg = self._apt_cache[start] 45 | else: 46 | msg = "'{}' not in apt cache.".format(start) 47 | if not self._quiet: 48 | sys.stderr.write(msg + '\n') 49 | raise KeyError(msg) 50 | if target in self._apt_cache: 51 | target_pkg = self._apt_cache[target] 52 | else: 53 | msg = "'{}' not in apt cache.".format(target) 54 | if not self._quiet: 55 | sys.stderr.write(msg + '\n') 56 | raise KeyError(msg) 57 | 58 | self._edges_to_target = [] 59 | self._deferred_pkgs = set() 60 | if not cache: 61 | cache = TracerCache() 62 | self._cache = cache 63 | 64 | # Descend through dependency 65 | self._trace_path(start_pkg, target_pkg) 66 | # Need extra passes for circular dependencies 67 | while self._deferred_pkgs: 68 | def_pkgs = self._deferred_pkgs 69 | self._deferred_pkgs = set() 70 | for def_pkg in def_pkgs: 71 | self._trace_path(def_pkg, target_pkg) 72 | # TODO(sloretz) why are edges sometimes repeated? 73 | return list(set(self._edges_to_target)) 74 | 75 | def _trace_path(self, start, target): 76 | """Depth first search to trace paths to target.""" 77 | if start.name == target.name: 78 | # Found target 79 | return True 80 | start_node = Node(start.name, APT_NODE) 81 | if self._cache.check_visited(start_node): 82 | if self._cache.check_fully_explored(start_node): 83 | leads_to_target = self._cache.check_leads_to_target(start_node) 84 | if leads_to_target: 85 | self._edges_to_target.extend(self._cache.recursive_edges(start_node)) 86 | return leads_to_target 87 | else: 88 | # Defer checks on circular dependencies 89 | self._deferred_pkgs.add(start) 90 | return False 91 | self._cache.visit(start_node) 92 | if not start.candidate.dependencies: 93 | # lowest level and target not found 94 | self._cache.mark_leads_to_target(start_node, False) 95 | return False 96 | leads_to_target = False 97 | for dependency in start.candidate.dependencies: 98 | # Check all the candidates that can satisfy this dependency 99 | for base_dep in dependency: 100 | # Only walk upstream dependencies 101 | if base_dep.rawtype in ['Depends', 'PreDepends', 'Suggests', 'Recommends']: 102 | if self._apt_cache.is_virtual_package(base_dep.name): 103 | virtual_to_target = False 104 | base_dep_node = Node(base_dep.name, APT_NODE) 105 | for pkg in self._apt_cache.get_providing_packages(base_dep.name): 106 | if self._trace_path(pkg, target): 107 | leads_to_target = True 108 | edge1 = Edge(start_node, base_dep.rawtype, base_dep_node) 109 | pkg_node = Node(pkg.name, APT_NODE) 110 | edge2 = Edge(base_dep_node, 'virtual', pkg_node) 111 | self._cache.add_edge(edge1) 112 | self._cache.add_edge(edge2) 113 | self._edges_to_target.append(edge1) 114 | self._edges_to_target.append(edge2) 115 | virtual_to_target = True 116 | self._cache.mark_leads_to_target(base_dep_node, virtual_to_target) 117 | else: 118 | if base_dep.name in self._apt_cache: 119 | pkg = self._apt_cache[base_dep.name] 120 | else: 121 | if not self._quiet: 122 | sys.stderr.write( 123 | "'{}' not in apt cache. Used by '{}' as '{}'\n".format( 124 | base_dep.name, start.name, dependency)) 125 | continue 126 | if self._trace_path(pkg, target): 127 | leads_to_target = True 128 | pkg_node = Node(pkg.name, APT_NODE) 129 | edge = Edge(start_node, base_dep.rawtype, pkg_node) 130 | self._cache.add_edge(edge) 131 | self._edges_to_target.append(edge) 132 | self._cache.mark_leads_to_target(start_node, leads_to_target) 133 | return leads_to_target 134 | 135 | 136 | APT_EDGE_LEGEND = { 137 | 'virtual': '[color=green]', 138 | 'Depends': '[color=blue]', 139 | 'PreDepends': '[color=blue]', 140 | 'Suggests': '[color=yellow]', 141 | 'Recommends': '[color=yellow]', 142 | } 143 | 144 | 145 | class AptTracerCommand(object): 146 | 147 | COMMAND_NAME='check-apt' 148 | COMMAND_HELP='check if apt package depends on python 2' 149 | 150 | def __init__(self, parser): 151 | # arguments for start, target, quiet, and dot output 152 | # Add arguments to arg-parser 153 | parser.add_argument( 154 | 'pkg', type=str, 155 | help='Name of package to check for dependency on python 2') 156 | parser.add_argument('--quiet', action='store_true') 157 | parser.add_argument( 158 | '--dot', action='store_true', help='output DOT graph') 159 | parser.add_argument( 160 | '--target', default='python', 161 | help='Package to trace to (default python)') 162 | 163 | def do_command(self, args): 164 | start = args.pkg 165 | target = args.target 166 | 167 | tracer = AptTracer(quiet=args.quiet) 168 | 169 | try: 170 | paths = tracer.trace_paths(start, target) 171 | except KeyError: 172 | return 2 173 | 174 | if args.dot: 175 | print(paths_to_dot(paths, edge_legend=APT_EDGE_LEGEND)) 176 | elif not args.quiet: 177 | if paths: 178 | print('{} depends on {}'.format(start, target)) 179 | else: 180 | print('{} does not depend on {}'.format(start, target)) 181 | 182 | if paths: 183 | # non-zero exit code to indicate it does depend on target 184 | # because it's assumed depending on target is undesirable 185 | return 1 186 | return 0 187 | -------------------------------------------------------------------------------- /py3_ready/cli.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Open Source Robotics Foundation, 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 sys 16 | import argparse 17 | 18 | 19 | def please_install(module, debian_package_suffix): 20 | sys.exit("""Unable to import {module}. Please install it on this system. 21 | 22 | sudo apt-get install python{version}-{suffix}""".format( 23 | module=module, version= sys.version_info.major, 24 | suffix=debian_package_suffix)) 25 | 26 | 27 | def main(): 28 | 29 | try: 30 | import apt 31 | except ImportError: 32 | please_install('apt', 'apt') 33 | from .apt_tracer import AptTracerCommand 34 | 35 | try: 36 | import rosdep2 37 | except ImportError: 38 | please_install('rosdep2', 'rosdep-modules') 39 | from .rosdep import CheckRosdepCommand 40 | 41 | try: 42 | import catkin_pkg 43 | except ImportError: 44 | please_install('catkin_pkg', 'catkin-pkg-modules') 45 | from .package_xml import CheckPackageCommand 46 | 47 | parser = argparse.ArgumentParser() 48 | subparsers = parser.add_subparsers() 49 | 50 | cmd_classes = [ 51 | AptTracerCommand, 52 | CheckRosdepCommand, 53 | CheckPackageCommand 54 | ] 55 | 56 | for cmd_class in cmd_classes: 57 | sub_parser = subparsers.add_parser( 58 | cmd_class.COMMAND_NAME, help=cmd_class.COMMAND_HELP) 59 | cmd = cmd_class(sub_parser) 60 | sub_parser.set_defaults(func=cmd.do_command) 61 | 62 | args = parser.parse_args() 63 | if not hasattr(args, 'func'): 64 | parser.print_usage() 65 | else: 66 | sys.exit(args.func(args)) 67 | -------------------------------------------------------------------------------- /py3_ready/dependency_tracer.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Open Source Robotics Foundation, 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 | """Interface for tracing package dependencies.""" 16 | 17 | 18 | class DependencyTracer(object): 19 | 20 | def trace_paths(self, start, target): 21 | raise NotImplementedError() 22 | 23 | 24 | class Node(object): 25 | 26 | __slots__ = ( 27 | 'name', 28 | 'node_type' 29 | ) 30 | 31 | def __init__(self, name, node_type): 32 | self.name = name 33 | self.node_type = node_type 34 | 35 | def __key(self): 36 | return (self.name, self.node_type) 37 | 38 | def __hash__(self): 39 | return hash(self.__key()) 40 | 41 | def __eq__(self, other): 42 | if isinstance(other, Node): 43 | return self.__key() == other.__key() 44 | return NotImplemented 45 | 46 | 47 | class Edge(object): 48 | 49 | __slots__ = ( 50 | 'start', 51 | 'edge_type', 52 | 'end', 53 | ) 54 | 55 | def __init__(self, start, edge_type, end): 56 | self.start = start 57 | self.edge_type = edge_type 58 | self.end = end 59 | 60 | def __key(self): 61 | return (self.start, self.edge_type, self.end) 62 | 63 | def __hash__(self): 64 | return hash(self.__key()) 65 | 66 | def __eq__(self, other): 67 | if isinstance(other, Edge): 68 | return self.__key() == other.__key() 69 | return NotImplemented 70 | 71 | 72 | class TracerCache(object): 73 | """Caches edges and dead ends to target.""" 74 | 75 | def __init__(self): 76 | # Key is (name, rawtype) value is True/False if node leads to target 77 | self._visited_nodes = {} # type: Dict[(str, str), Optional[bool]] 78 | self._edges = {} # type: Dict[(str, str), Set[(str, str)]] 79 | 80 | def visit(self, node): 81 | if node not in self._visited_nodes: 82 | self._visited_nodes[node] = None 83 | 84 | def check_visited(self, node): 85 | if node in self._visited_nodes: 86 | return True 87 | 88 | def check_leads_to_target(self, node): 89 | if node in self._visited_nodes: 90 | return self._visited_nodes[node] 91 | 92 | def check_fully_explored(self, node): 93 | if node in self._visited_nodes: 94 | return self._visited_nodes[node] is not None 95 | return False 96 | 97 | def edges(self, node): 98 | if node in self._edges: 99 | for edge in self._edges[node]: 100 | yield edge 101 | 102 | def recursive_edges(self, node): 103 | 104 | def _recursive_edges(node, edges): 105 | for edge in self.edges(node): 106 | if edge not in edges: 107 | edges.add(edge) 108 | _recursive_edges(edge.end, edges) 109 | 110 | edges = set() 111 | _recursive_edges(node, edges) 112 | for edge in edges: 113 | yield edge 114 | 115 | def add_edge(self, edge): 116 | if edge.start not in self._edges: 117 | self._edges[edge.start] = set() 118 | self._edges[edge.start].add(edge) 119 | 120 | def mark_leads_to_target(self, node, gets_there): 121 | self._visited_nodes[node] = gets_there 122 | -------------------------------------------------------------------------------- /py3_ready/dot.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Open Source Robotics Foundation, 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 | 16 | def paths_to_dot(paths, edge_legend=None, node_legend=None): 17 | """Given dependency paths, output in dot format for graphviz.""" 18 | if edge_legend is None: 19 | edge_legend = {} 20 | if node_legend is None: 21 | node_legend = {} 22 | edges = [] 23 | nodes = set() 24 | for edge in paths: 25 | style = '' 26 | if edge.edge_type in edge_legend: 27 | style = edge_legend[edge.edge_type] 28 | edges.append(' "{beg}%{begtype}" -> "{end}%{endtype}"{style}; // {rawtype}\n'.format( 29 | beg=edge.start.name, 30 | begtype=edge.start.node_type, 31 | end=edge.end.name, 32 | endtype=edge.end.node_type, 33 | style=style, 34 | rawtype=edge.edge_type)) 35 | nodes.add(edge.start) 36 | nodes.add(edge.end) 37 | node_dot = [] 38 | for node in nodes: 39 | style = '' 40 | if node.node_type in node_legend: 41 | style = node_legend[node.node_type] 42 | node_dot.append(' "{name}%{ntype}"{style}[label="{name}"]; // {ntype}\n'.format( 43 | name=node.name, style=style, ntype=node.node_type)) 44 | 45 | return 'digraph G {{\n{edges}\n{nodes}}}'.format( 46 | edges=''.join(edges), 47 | nodes=''.join(node_dot)) 48 | -------------------------------------------------------------------------------- /py3_ready/package_xml.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Open Source Robotics Foundation, 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 | """Tools for checking dependencies of package.xml files.""" 16 | 17 | from __future__ import print_function 18 | 19 | import os 20 | import sys 21 | 22 | from .apt_tracer import APT_EDGE_LEGEND 23 | from .dependency_tracer import DependencyTracer 24 | from .dependency_tracer import Edge 25 | from .dependency_tracer import Node 26 | from .dependency_tracer import TracerCache 27 | from .dot import paths_to_dot 28 | from .rosdep import is_rosdep_initialized 29 | from .rosdep import ROSDEP_EDGE_LEGEND 30 | from .rosdep import ROSDEP_NODE 31 | from .rosdep import ROSDEP_NODE_LEGEND 32 | from .rosdep import RosdepTracer 33 | 34 | from apt.cache import Cache 35 | from catkin_pkg.package import parse_package 36 | from catkin_pkg.packages import find_packages 37 | 38 | 39 | PACKAGE_NODE='package' 40 | 41 | 42 | class PackageCache(object): 43 | 44 | def __init__(self): 45 | # Key: name, Value: parsed package xml file 46 | self._packages = self._find_packages(self._get_search_paths()) 47 | 48 | def _get_search_paths(self): 49 | env_vars = ( 50 | 'AMENT_PREFIX_PATH', 51 | 'CMAKE_PREFIX_PATH', 52 | 'COLCON_PREFIX_PATH' 53 | ) 54 | paths = [] 55 | for var in env_vars: 56 | text = os.getenv(var, default='') 57 | for path in text.split(':'): 58 | if path: 59 | yield path 60 | 61 | def _find_packages(self, search_paths): 62 | packages = {} 63 | for path in search_paths: 64 | for path, pkg in find_packages(path).items(): 65 | # TODO(sloretz) is this doing overlay workspaces correctly? 66 | packages[pkg.name] = pkg 67 | return packages 68 | 69 | def find_package(self, name): 70 | if name in self._packages: 71 | return self._packages[name] 72 | 73 | 74 | 75 | class PackageTracer(DependencyTracer): 76 | 77 | def __init__(self, apt_cache=None, quiet=True): 78 | self._quiet = quiet 79 | self._tracer = RosdepTracer(apt_cache=apt_cache, quiet=self._quiet) 80 | self._package_cache = PackageCache() 81 | 82 | def trace_paths(self, start, target, cache=None): 83 | # start: name of a ROS package 84 | # target: name of a debian package 85 | start_pkg = self._package_cache.find_package(start) 86 | if start_pkg is None: 87 | if not self._quiet: 88 | sys.stderr.write('Failed to find package "{}".' 89 | ' Did you remember to source the workspace?\n'.format(start)) 90 | raise KeyError() 91 | 92 | if not cache: 93 | cache = TracerCache() 94 | self._cache = cache 95 | 96 | self._visited_pkgs = [] 97 | self._deferred_pkgs = set() 98 | self._pkgs_to_target = set([]) 99 | self._visited_rosdeps = [] 100 | self._rosdeps_to_target = set([]) 101 | 102 | self._trace_path(start_pkg, target) 103 | # Need extra passes for circular dependencies 104 | while self._deferred_pkgs: 105 | def_pkgs = self._deferred_pkgs 106 | self._deferred_pkgs = set() 107 | for def_pkg in def_pkgs: 108 | self._trace_path(def_pkg, target) 109 | start_node = Node(start_pkg.name, PACKAGE_NODE) 110 | return list(set(self._cache.recursive_edges(start_node))) 111 | 112 | def _trace_path(self, start, target): 113 | """return true if path leads to target debian package""" 114 | start_node = Node(start.name, PACKAGE_NODE) 115 | if self._cache.check_visited(start_node): 116 | if self._cache.check_fully_explored(start_node): 117 | leads_to_target = self._cache.check_leads_to_target(start_node) 118 | return leads_to_target 119 | else: 120 | # Defer checks on circular dependencies 121 | self._deferred_pkgs.add(start) 122 | return False 123 | self._cache.visit(start_node) 124 | 125 | depends = [] 126 | for dep in start.build_depends: 127 | depends.append((dep, 'build_depend')) 128 | for dep in start.buildtool_depends: 129 | depends.append((dep, 'buildtool_depend')) 130 | for dep in start.build_export_depends: 131 | depends.append((dep, 'build_export_depend')) 132 | for dep in start.buildtool_export_depends: 133 | depends.append((dep, 'buildtool_export_depend')) 134 | for dep in start.exec_depends: 135 | depends.append((dep, 'exec_depend')) 136 | for dep in start.test_depends: 137 | depends.append((dep, 'test_depend')) 138 | for dep in start.doc_depends: 139 | depends.append((dep, 'doc_depend')) 140 | for dep in start.group_depends: 141 | depends.append((dep, 'group_depend')) 142 | 143 | rosdep_keys = [] 144 | for dep, _ in depends: 145 | if self._package_cache.find_package(dep.name) is None: 146 | rosdep_keys.append(dep.name) 147 | 148 | leads_to_target = False 149 | for dep, rawtype in depends: 150 | if dep.name in rosdep_keys: 151 | dep_node = Node(dep.name, ROSDEP_NODE) 152 | dep_leads_to_target = False 153 | if self._cache.check_fully_explored(dep_node): 154 | dep_leads_to_target = self._cache.check_leads_to_target(dep_node) 155 | else: 156 | # Trace rosdep key to target 157 | rosdep_paths = self._tracer.trace_paths(dep.name, target, cache=self._cache) 158 | if rosdep_paths: 159 | dep_leads_to_target = True 160 | if dep_leads_to_target: 161 | leads_to_target = True 162 | edge = Edge( 163 | start_node, 164 | rawtype, 165 | dep_node 166 | ) 167 | self._cache.add_edge(edge) 168 | leads_to_target = True 169 | else: 170 | pkg = self._package_cache.find_package(dep.name) 171 | if pkg is None and not self._quiet: 172 | sys.stderr.write('Failed to find package or rosdep key [{}]\n'.format(dep.name)) 173 | dep_node = Node(dep.name, PACKAGE_NODE) 174 | if self._trace_path(pkg, target): 175 | edge = Edge(start_node, rawtype, dep_node) 176 | self._cache.add_edge(edge) 177 | leads_to_target = True 178 | 179 | self._cache.mark_leads_to_target(start_node, leads_to_target) 180 | return leads_to_target 181 | 182 | 183 | PACKAGE_EDGE_LEGEND = { 184 | 'build_depend': '[color=pink]', 185 | 'buildtool_depend': '[color=pink]', 186 | 'build_export_depend': '[color=pink]', 187 | 'buildtool_export_depend': '[color=pink]', 188 | 'exec_depend': '[color=pink]', 189 | 'test_depend': '[color=pink]', 190 | 'doc_depend': '[color=pink]', 191 | 'group_depend': '[color=pink]', 192 | } 193 | 194 | PACKAGE_NODE_LEGEND = { 195 | PACKAGE_NODE: '[color=pink,shape=hexagon]' 196 | } 197 | 198 | 199 | class CheckPackageCommand(object): 200 | COMMAND_NAME='check-package' 201 | COMMAND_HELP='check if a ROS package depends on python 2' 202 | 203 | def __init__(self, parser): 204 | # arguments for key, quiet, and dot output 205 | parser.add_argument( 206 | 'package', type=str, 207 | help='Name of a package to check') 208 | parser.add_argument('--quiet', action='store_true') 209 | parser.add_argument( 210 | '--dot', action='store_true', help='output DOT graph') 211 | parser.add_argument( 212 | '--target', default='python', 213 | help='Debian package to trace to (default python)') 214 | 215 | def do_command(self, args): 216 | all_paths = [] 217 | tracer = PackageTracer(quiet=args.quiet) 218 | 219 | try: 220 | all_paths = tracer.trace_paths(args.package, args.target) 221 | except OSError as e: 222 | sys.stderr.write(str(e) + '\n') 223 | return 2 224 | except KeyError: 225 | return 2 226 | 227 | if args.dot: 228 | edge_legend = {} 229 | edge_legend.update(APT_EDGE_LEGEND) 230 | edge_legend.update(ROSDEP_EDGE_LEGEND) 231 | edge_legend.update(PACKAGE_EDGE_LEGEND) 232 | node_legend = {} 233 | node_legend.update(ROSDEP_NODE_LEGEND) 234 | node_legend.update(PACKAGE_NODE_LEGEND) 235 | print(paths_to_dot( 236 | all_paths, 237 | edge_legend=edge_legend, 238 | node_legend=node_legend)) 239 | elif not args.quiet: 240 | if all_paths: 241 | print('{} depends on {}'.format(args.package, args.target)) 242 | else: 243 | print('{} does not depend on {}'.format(args.path, args.target)) 244 | 245 | if all_paths: 246 | # non-zero exit code to indicate it does depend on target 247 | # because it's assumed depending on target is undesirable 248 | return 1 249 | return 0 250 | -------------------------------------------------------------------------------- /py3_ready/rosdep.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Open Source Robotics Foundation, 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 | """Tools for checking dependencies of rosdep packages.""" 16 | 17 | from __future__ import print_function 18 | 19 | import os 20 | import sys 21 | 22 | from .apt_tracer import APT_EDGE_LEGEND 23 | from .apt_tracer import APT_NODE 24 | from .apt_tracer import AptTracer 25 | from .dependency_tracer import DependencyTracer 26 | from .dependency_tracer import Edge 27 | from .dependency_tracer import Node 28 | from .dependency_tracer import TracerCache 29 | from .dot import paths_to_dot 30 | 31 | from apt.cache import Cache 32 | from rosdep2 import create_default_installer_context 33 | from rosdep2 import get_default_installer 34 | from rosdep2.lookup import RosdepLookup 35 | from rosdep2.platforms.debian import AptInstaller 36 | from rosdep2.rospkg_loader import DEFAULT_VIEW_KEY 37 | from rosdep2.sources_list import CACHE_INDEX 38 | from rosdep2.sources_list import get_sources_cache_dir 39 | from rosdep2.sources_list import get_sources_list_dir 40 | from rosdep2.sources_list import SourcesListLoader 41 | 42 | 43 | ROSDEP_NODE = 'rosdep' 44 | 45 | 46 | def is_rosdep_initialized(): 47 | sources_cache_dir = get_sources_cache_dir() 48 | filename = os.path.join(sources_cache_dir, CACHE_INDEX) 49 | if os.path.exists(filename): 50 | return True 51 | else: 52 | return False 53 | 54 | sources_list_dir = get_sources_list_dir() 55 | if not os.path.exists(sources_list_dir): 56 | return False 57 | else: 58 | filelist = [f for f in os.listdir(sources_list_dir) if f.endswith('.list')] 59 | if not filelist: 60 | return False 61 | return True 62 | 63 | 64 | def resolve_rosdep_key(key, quiet=False): 65 | sources_loader = SourcesListLoader.create_default( 66 | sources_cache_dir=get_sources_cache_dir(), 67 | os_override=None, 68 | verbose=False) 69 | lookup = RosdepLookup.create_from_rospkg(sources_loader=sources_loader) 70 | lookup.verbose = False 71 | 72 | installer_context = create_default_installer_context(verbose=False) 73 | 74 | installer, installer_keys, default_key, \ 75 | os_name, os_version = get_default_installer( 76 | installer_context=installer_context, 77 | verbose=False) 78 | 79 | view = lookup.get_rosdep_view(DEFAULT_VIEW_KEY, verbose=False) 80 | try: 81 | d = view.lookup(key) 82 | except KeyError as e: 83 | sys.stderr.write('Invalid key "{}": {}\n'.format(key, e)) 84 | return 85 | 86 | rule_installer, rule = d.get_rule_for_platform( 87 | os_name, os_version, installer_keys, default_key) 88 | 89 | installer = installer_context.get_installer(rule_installer) 90 | resolved = installer.resolve(rule) 91 | 92 | for error in lookup.get_errors(): 93 | if not quiet: 94 | print('WARNING: %s' % (error), file=sys.stderr) 95 | 96 | return {installer: resolved} 97 | 98 | 99 | class RosdepTracer(DependencyTracer): 100 | 101 | def __init__(self, apt_cache=None, quiet=True): 102 | self._quiet = quiet 103 | self._tracer = AptTracer(apt_cache=apt_cache, quiet=self._quiet) 104 | 105 | def trace_paths(self, start, target, cache=None): 106 | start_node = Node(start, ROSDEP_NODE) 107 | if not cache: 108 | cache = TracerCache() 109 | if cache.check_fully_explored(start_node): 110 | return [e for e in cache.recursive_edges(start_node)] 111 | if not is_rosdep_initialized(): 112 | msg = ('The rosdep database is not ready to be used. ' 113 | 'Run \n\n\trosdep resolve {}\n\n' 114 | 'for instructions on how to fix this.\n'.format(start)) 115 | if not self._quiet: 116 | print(msg) 117 | raise KeyError(msg) 118 | 119 | resolved = resolve_rosdep_key(start) 120 | if resolved is None: 121 | msg = 'Could not resolve rosdep key {}\n'.format(start) 122 | if not self._quiet: 123 | print(msg) 124 | raise KeyError(msg) 125 | 126 | apt_depends = [] 127 | for installer, pkgs in resolved.items(): 128 | if isinstance(installer, AptInstaller): 129 | apt_depends = pkgs 130 | 131 | all_paths = [] 132 | if not apt_depends: 133 | if not self._quiet: 134 | sys.stderr.write( 135 | '{} did not resolve to an apt package\n'.format(start)) 136 | else: 137 | for apt_depend in apt_depends: 138 | paths = self._tracer.trace_paths(apt_depend, target, cache=cache) 139 | if paths: 140 | apt_node = Node(apt_depend, APT_NODE) 141 | edge = Edge(start_node, 'rosdep', apt_node) 142 | cache.add_edge(edge) 143 | paths.append(edge) 144 | all_paths.extend(paths) 145 | cache.mark_leads_to_target(start_node, bool(all_paths)) 146 | return all_paths 147 | 148 | 149 | ROSDEP_EDGE_LEGEND = { 150 | 'rosdep': '[color=orange]', 151 | } 152 | 153 | ROSDEP_NODE_LEGEND = { 154 | ROSDEP_NODE: '[color=orange,shape=rect]', 155 | } 156 | 157 | 158 | class CheckRosdepCommand: 159 | COMMAND_NAME='check-rosdep' 160 | COMMAND_HELP='check if rosdep key depends on python 2' 161 | 162 | def __init__(self, parser): 163 | # arguments for key, quiet, and dot output 164 | parser.add_argument( 165 | 'key', type=str, 166 | help='rosdep key to check for dependency on python 2') 167 | parser.add_argument('--quiet', action='store_true') 168 | parser.add_argument( 169 | '--dot', action='store_true', help='output DOT graph') 170 | parser.add_argument( 171 | '--target', default='python', 172 | help='Debian package to trace to (default python)') 173 | 174 | def do_command(self, args): 175 | tracer = RosdepTracer(quiet=args.quiet) 176 | 177 | try: 178 | all_paths = tracer.trace_paths(args.key, args.target) 179 | except KeyError: 180 | return 2 181 | 182 | if args.dot: 183 | edge_legend = {} 184 | edge_legend.update(APT_EDGE_LEGEND) 185 | edge_legend.update(ROSDEP_EDGE_LEGEND) 186 | print( 187 | paths_to_dot(list(set(all_paths)), 188 | edge_legend=edge_legend, 189 | node_legend=ROSDEP_NODE_LEGEND)) 190 | elif not args.quiet: 191 | if all_paths: 192 | print('rosdep key {} depends on {}'.format(args.key, args.target)) 193 | else: 194 | print('rosdep key {} does not depend on {}'.format(args.key, args.target)) 195 | 196 | if all_paths: 197 | # non-zero exit code to indicate it does depend on target 198 | # because it's assumed depending on target is undesirable 199 | return 1 200 | return 0 201 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Open Source Robotics Foundation, 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 setuptools import setup 16 | 17 | with open("README.rst", "r") as fin: 18 | long_description = fin.read() 19 | 20 | setup( 21 | name='py3-ready', 22 | version='0.1.0', 23 | packages=['py3_ready'], 24 | entry_points={ 25 | 'console_scripts': [ 26 | 'py3-ready = py3_ready.cli:main', 27 | ] 28 | }, 29 | author='Shane Loretz', 30 | author_email='sloretz@openrobotics.org', 31 | classifiers=[ 32 | 'Programming Language :: Python', 33 | 'Programming Language :: Python :: 3', 34 | 'Programming Language :: Python :: 2', 35 | 'License :: OSI Approved :: Apache Software License' 36 | ], 37 | description='A tool to identify dependencies on python 2.', 38 | long_description=long_description, 39 | long_description_content_type="text/x-rst", 40 | license='Apache License 2.0', 41 | url='https://github.com/osrf/py3-ready', 42 | install_requires=[], # TODO(sloretz) what to do if deps are debian packages? 43 | ) 44 | --------------------------------------------------------------------------------