pyporter 17.8 KB
Newer Older
Z
Zhipeng Xie 已提交
1 2 3 4 5
#!/usr/bin/python3
"""
This is a packager bot for python modules from pypi.org
"""
#******************************************************************************
6
# Copyright (c) Huawei Technologies Co., Ltd. 2020-2020. All rights reserved.
Z
Zhipeng Xie 已提交
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
# licensed under the Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
#     http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR
# PURPOSE.
# See the Mulan PSL v2 for more details.
# Author: Shinwell_Hu Myeuler
# Create: 2020-05-07
# Description: provide a tool to package python module automatically
# ******************************************************************************/

import urllib
import urllib.request
from pprint import pprint
from os import path
import json
import sys
import re
import datetime
import argparse
import subprocess
import os
31
import platform
Z
Zhipeng Xie 已提交
32 33 34 35
from pathlib import Path
# python3-wget is not default available on openEuler yet.
# import wget  

36

Z
Zhipeng Xie 已提交
37
json_file_template = '{pkg_name}.json'
X
xw 已提交
38
name_tag_template    = 'Name:\t\t{pkg_name}' 
Z
Zhipeng Xie 已提交
39 40 41 42 43 44 45 46 47 48 49 50 51 52
summary_tag_template = 'Summary:\t{pkg_sum}' 
version_tag_template = 'Version:\t{pkg_ver}' 
release_tag_template = 'Release:\t1'
license_tag_template = 'License:\t{pkg_lic}' 
home_tag_template = 'URL:\t\t{pkg_home}' 
source_tag_template = 'Source0:\t{pkg_source}'

buildreq_tag_template = 'BuildRequires:\t{req}'

# TODO List
# 1. Need a reliable way to get description of module .. Partially done
# 2. requires_dist has some dependency restirction, need to present
# 3. dependency outside python (i.e. pycurl depends on libcurl) doesn't exist in pipy

M
myeuler 已提交
53 54


X
xw 已提交
55 56 57 58
class PyPorter:
    __url_template = 'https://pypi.org/pypi/{pkg_name}/json'
    __build_noarch = True
    __json = None
X
xw 已提交
59 60 61
    __module_name = ""
    __spec_name = ""
    __pkg_name = ""
X
xw 已提交
62 63 64 65 66 67 68 69 70
    
    def __init__(self, pkg):
        """
        receive json from pypi.org
        """
        url = self.__url_template.format(pkg_name=pkg)
        resp = ""
        with urllib.request.urlopen(url) as u:
            self.__json = json.loads(u.read().decode('utf-8'))
X
xw 已提交
71 72 73 74 75 76
        if (self.__json is not None):
            self.__module_name = self.__json["info"]["name"]
            self.__spec_name = "python-" + self.__module_name
            self.__pkg_name = "python3-" + self.__module_name
            self.__build_noarch = self.__get_buildarch()

X
xw 已提交
77

M
myeuler 已提交
78
    def get_spec_name(self):
X
xw 已提交
79 80 81 82
        return self.__spec_name 

    def get_module_name(self):
        return self.__module_name
M
myeuler 已提交
83

X
xw 已提交
84 85
    def get_pkg_name(self):
        return self.__pkg_name
M
myeuler 已提交
86 87 88 89 90 91 92 93 94 95

    def get_version(self):
        return self.__json["info"]["version"]
    
    def get_summary(self):
        return self.__json["info"]["summary"]

    def get_home(self):
        return self.__json["info"]["project_urls"]["Homepage"]

X
xw 已提交
96 97 98 99 100 101 102 103 104 105 106 107 108
    def get_license(self):
        """
        By default, the license info can be achieved from json["info"]["license"]
        in rare cases it doesn't work.
        We fall back to json["info"]["classifiers"], it looks like License :: OSI Approved :: BSD Clause
        """
        if self.__json["info"]["license"] != "":
            return self.__json["info"]["license"]
        for k in self.__json["info"]["classifiers"]:
            if k.startswith("License"):
                ks = k.split("::")
                return ks[2].strip()
        return ""
Z
Zhipeng Xie 已提交
109

X
xw 已提交
110
    def get_source_url(self):
X
xw 已提交
111 112 113 114
        """
        return URL for source file for the latest version
        return "" in errors
        """
X
xw 已提交
115 116
        v = self.__json["info"]["version"]
        rs = self.__json["releases"][v]
X
xw 已提交
117 118 119 120
        for r in rs:
            if r["packagetype"] == "sdist":
                return r["url"]
        return ""
Z
Zhipeng Xie 已提交
121

X
xw 已提交
122
    def get_requires(self):
X
xw 已提交
123 124 125
        """
        return all requires no matter if extra is required.
        """
X
xw 已提交
126
        rs = self.__json["info"]["requires_dist"]
X
xw 已提交
127 128 129 130 131 132
        if rs is None:
            return
        for r in rs:
            idx = r.find(";")
            mod = transform_module_name(r[:idx])
            print("Requires:\t" + mod)
X
xw 已提交
133 134 135
        if self.__json["info"]["license"] != "":
            return self.__json["info"]["license"]
        for k in self.__json["info"]["classifiers"]:
X
xw 已提交
136 137 138 139
            if k.startswith("License"):
                ks = k.split("::")
                return ks[2].strip()
        return ""
Z
Zhipeng Xie 已提交
140

X
xw 已提交
141
    def __get_buildarch(self):
X
xw 已提交
142 143 144 145
        """
        if this module has a prebuild package for amd64, then it is arch dependent.
        print BuildArch tag if needed.
        """
M
myeuler 已提交
146 147
        v = self.__json["info"]["version"]
        rs = self.__json["releases"][v]
X
xw 已提交
148 149 150
        for r in rs:
            if r["packagetype"] == "bdist_wheel":
                if r["url"].find("amd64") != -1:
X
xw 已提交
151 152 153 154 155 156 157 158 159
                    return False
        return True

    def is_build_noarch(self):
        return self.__build_noarch

    def get_buildarch(self):
        if (self.__build_noarch == True):
            print("BuildArch:\tnoarch")
X
xw 已提交
160

M
myeuler 已提交
161
    def get_description(self):
X
xw 已提交
162 163 164 165 166 167
        """
        return description.
        Usually it's json["info"]["description"]
        If it's rst style, then only use the content for the first paragraph, and remove all tag line.
        For empty description, use summary instead.
        """
M
myeuler 已提交
168
        desc = self.__json["info"]["description"].splitlines()
X
xw 已提交
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
        res = []
        paragraph = 0
        for d in desc:
            if len(d.strip()) == 0:
                continue
            first_char = d.strip()[0]
            ignore_line = False
            if d.strip().startswith("===") or d.strip().startswith("---"):
                paragraph = paragraph + 1
                ignore_line = True
            elif d.strip().startswith(":") or d.strip().startswith(".."):
                ignore_line = True
            if ignore_line != True and paragraph == 1:
                res.append(d)
            if paragraph >= 2:
                del res[-1]
                return "\n".join(res)
        if res != []:
            return "\n".join(res)
        elif paragraph == 0:
M
myeuler 已提交
189
            return self.__json["info"]["description"]
X
xw 已提交
190
        else:
M
myeuler 已提交
191
            return self.__json["info"]["summary"]
X
xw 已提交
192

X
xw 已提交
193
    def get_build_requires(self):
X
xw 已提交
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
        req_list=[]
        rds = self.__json["info"]["requires_dist"]
        if rds is not None:
            for rp in rds:
                br = refine_requires(rp)
                if (br == ""):
                    continue
                #
                # Do not output BuildRequires: 
                # just collect all build requires and using pip to install
                # than can help to build all rpm withoud trap into 
                # build dependency nightmare
                #
                #print(buildreq_tag_template.format(req=br))
                name=str.lstrip(br).split(" ")
                req_list.append(name[0])
        return req_list
Z
Zhipeng Xie 已提交
211

X
xw 已提交
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244
    def prepare_build_requires(self):
        print(buildreq_tag_template.format(req='python3-devel'))
        print(buildreq_tag_template.format(req='python3-setuptools'))
        if (self.__build_noarch == False):
            print(buildreq_tag_template.format(req='python3-cffi'))
            print(buildreq_tag_template.format(req='gcc'))
            print(buildreq_tag_template.format(req='gdb'))

    def prepare_pkg_build(self):
        print("%py3_build")
    def prepare_pkg_install(self):
        print("%py3_install")
    def prepare_pkg_files(self):
        if self.__build_noarch:
            print("%dir %{python3_sitelib}/*")
        else:
            print("%dir %{python3_sitearch}/*")
    
    def store_json(self, spath):
        """
        save json file
        """
        fname = json_file_template.format(pkg_name=self.__pkg_name)
        json_file = os.path.join(spath, fname)
        
        # if file exist, do nothing 
        if path.exists(json_file) and path.isfile(json_file):
            with open(json_file, 'r') as f:
                resp = json.load(f)
        else:
            with open(json_file, 'w') as f:
                json.dump(self.__json, f)

Z
Zhipeng Xie 已提交
245 246 247 248 249 250 251 252 253

def transform_module_name(n):
    """
    return module name with version restriction.
    Any string with '.' or '/' is considered file, and will be ignored
    Modules start with python- will be changed to python3- for consistency.
    """
    # remove ()
    ns = re.split("[()]", n)
254
    ver_constrain = []
Z
Zhipeng Xie 已提交
255 256 257 258 259 260 261
    ns[0] = ns[0].strip()
    if ns[0].startswith("python-"):
        ns[0] = ns[0].replace("python-", "python3-")
    else:
        ns[0] = "python3-" + ns[0] 
        if ns[0].find("/") != -1 or ns[0].find(".") != -1:
            return ""
262 263 264 265 266 267 268 269 270 271
    """
    if len(ns) > 1:
        vers = ns[1].split(",")
        for ver in vers:
            m = re.match("([<>=]+)( *)(\d.*)", ver.strip())
            ver_constrain.append(ns[0] + " " + m[1] + " " + m[3])
        return ", ".join(ver_constrain)
    else:
    """
    return ns[0]
Z
Zhipeng Xie 已提交
272 273 274 275 276 277 278 279 280 281 282 283


def refine_requires(req):
    """
    return only requires without ';' (thus no extra)
    """
    ra = req.split(";", 1)
    #
    # Do not add requires which has ;, which is often has very complicated precondition
    # TODO: need more parsing of the denpency after ;
    return transform_module_name(ra[0])

X
xw 已提交
284

X
xw 已提交
285
def download_source(porter, tgtpath):
Z
Zhipeng Xie 已提交
286 287 288 289 290 291
    """
    download source file from url, and save it to target path
    """
    if (os.path.exists(tgtpath) == False):
        print("download path %s does not exist\n", tgtpath)
        return False
X
xw 已提交
292
    s_url = porter.get_source_url()
Z
Zhipeng Xie 已提交
293 294 295
    return subprocess.call(["wget", s_url, "-P", tgtpath])


296
def prepare_rpm_build_env(root):
Z
Zhipeng Xie 已提交
297 298 299
    """
    prepare environment for rpmbuild
    """
300 301 302 303 304
    if (os.path.exists(root) == False):
        print("Root path %s does not exist\n" & buildroot)
        return ""

    buildroot = os.path.join(root, "rpmbuild")
Z
Zhipeng Xie 已提交
305
    if (os.path.exists(buildroot) == False):
306
        os.mkdir(buildroot)
Z
Zhipeng Xie 已提交
307 308 309 310 311 312

    for sdir in ['SPECS', 'BUILD', 'SOURCES', 'SRPMS', 'RPMS', 'BUILDROOT']:
        bpath = os.path.join(buildroot, sdir)
        if (os.path.exists(bpath) == False):
            os.mkdir(bpath)

313
    return buildroot
Z
Zhipeng Xie 已提交
314 315


316
def try_pip_install_package(pkg):
Z
Zhipeng Xie 已提交
317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
    """
    install packages listed in build requires
    """
    # try pip installation
    pip_name = pkg.split("-")
    if len(pip_name) == 2:
        ret = subprocess.call(["pip3", "install", "--user", pip_name[1]])
    else:
        ret = subprocess.call(["pip3", "install", "--user", pip_name[0]])

    if ret != 0:
        print("%s can not be installed correctly, Fix it later, go ahead to do building..." % pip_name)

    #
    # TODO: try to build anyway, fix it later
    #
    return True

335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
def package_installed(pkg):
    print(pkg)
    ret = subprocess.call(["rpm", "-qi", pkg])
    if ret == 0:
        return True

    return False


def dependencies_ready(req_list):
    """ 
    TODO: do not need to do dependency check here, do it in pyporter_run
    """
    #    if (try_pip_install_package(req) == False):
    #        return req
Z
Zhipeng Xie 已提交
350 351 352 353 354 355 356 357 358 359
    return ""

def build_package(specfile):
    """
    build rpm package with rpmbuild
    """
    ret = subprocess.call(["rpmbuild", "-ba", specfile])
    return ret


X
xw 已提交
360 361
def build_install_rpm(porter, rootpath):
    ret = build_rpm(porter, rootpath)
362 363 364 365
    if (ret != ""):
        return ret

    arch = "noarch"
X
xw 已提交
366 367 368
    if (porter.is_build_noarch()):
        arch = "noarch"
    else:
369 370
        arch = platform.machine()

X
xw 已提交
371
    pkgname = os.path.join(rootpath, "rpmbuild", "RPMS", arch, porter.get_pkg_name() + "*")
372 373 374 375 376 377
    ret = subprocess.call(["rpm", "-ivh", pkgname])
    if (ret != 0):
        return "Install failed\n"

    return ""

X
xw 已提交
378
def build_rpm(porter, rootpath):
Z
Zhipeng Xie 已提交
379 380 381
    """
    full process to build rpm
    """
382 383
    buildroot = prepare_rpm_build_env(rootpath) 
    if (buildroot == ""):
Z
Zhipeng Xie 已提交
384 385
        return False

X
xw 已提交
386
    specfile = os.path.join(buildroot, "SPECS", porter.get_spec_name() + ".spec")
Z
Zhipeng Xie 已提交
387

X
xw 已提交
388
    req_list = build_spec(porter, specfile)
389
    ret = dependencies_ready(req_list)
Z
Zhipeng Xie 已提交
390 391 392 393
    if ret != "":
        print("%s can not be installed automatically, Please handle it" % ret)
        return ret

X
xw 已提交
394
    download_source(porter, os.path.join(buildroot, "SOURCES"))
Z
Zhipeng Xie 已提交
395 396 397 398 399 400

    build_package(specfile)

    return ""


M
myeuler 已提交
401
def build_spec(porter, output):
Z
Zhipeng Xie 已提交
402 403 404 405
    """
    print out the spec file
    """
    if os.path.isdir(output):
X
xw 已提交
406
        output = os.path.join(output, porter.get_spec_name() + ".spec") 
Z
Zhipeng Xie 已提交
407
    tmp = sys.stdout
M
myeuler 已提交
408
    if (output != ""):
Z
Zhipeng Xie 已提交
409
        sys.stdout = open(output, 'w+')
410 411
   
    print("%global _empty_manifest_terminate_build 0")
X
xw 已提交
412
    print(name_tag_template.format(pkg_name=porter.get_spec_name()))
M
myeuler 已提交
413
    print(version_tag_template.format(pkg_ver=porter.get_version()))
Z
Zhipeng Xie 已提交
414
    print(release_tag_template)
M
myeuler 已提交
415 416 417 418 419
    print(summary_tag_template.format(pkg_sum=porter.get_summary()))
    print(license_tag_template.format(pkg_lic=porter.get_license()))
    print(home_tag_template.format(pkg_home=porter.get_home()))
    print(source_tag_template.format(pkg_source=porter.get_source_url()))
    porter.get_buildarch()
Z
Zhipeng Xie 已提交
420
    print("")
M
myeuler 已提交
421
    porter.get_requires()
Z
Zhipeng Xie 已提交
422 423
    print("")
    print("%description")
M
myeuler 已提交
424
    print(porter.get_description())
Z
Zhipeng Xie 已提交
425
    print("")
M
myeuler 已提交
426

X
xw 已提交
427 428 429
    print("%package -n {name}".format(name=porter.get_pkg_name()))
    print(summary_tag_template.format(pkg_sum=porter.get_summary()))
    print("Provides:\t" + porter.get_spec_name())
M
myeuler 已提交
430

X
xw 已提交
431
    porter.prepare_build_requires()
Z
Zhipeng Xie 已提交
432

M
myeuler 已提交
433
    build_req_list=porter.get_build_requires()
Z
Zhipeng Xie 已提交
434

X
xw 已提交
435 436
    print("%description -n " + porter.get_pkg_name())
    print(porter.get_description())
Z
Zhipeng Xie 已提交
437 438
    print("")
    print("%package help")
X
xw 已提交
439 440
    print("Summary:\tDevelopment documents and examples for {name}".format(name=porter.get_module_name()))
    print("Provides:\t{name}-doc".format(name=porter.get_pkg_name()))
Z
Zhipeng Xie 已提交
441
    print("%description help")
X
xw 已提交
442
    print(porter.get_description())
Z
Zhipeng Xie 已提交
443 444
    print("")
    print("%prep")
X
xw 已提交
445
    print("%autosetup -n {name}-{ver}".format(name=porter.get_module_name(), ver=porter.get_version()))
Z
Zhipeng Xie 已提交
446 447
    print("")
    print("%build")
X
xw 已提交
448
    porter.prepare_pkg_build()
Z
Zhipeng Xie 已提交
449 450
    print("")
    print("%install")
X
xw 已提交
451
    porter.prepare_pkg_install()
Z
Zhipeng Xie 已提交
452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469
    print("install -d -m755 %{buildroot}/%{_pkgdocdir}")
    print("if [ -d doc ]; then cp -arf doc %{buildroot}/%{_pkgdocdir}; fi")
    print("if [ -d docs ]; then cp -arf docs %{buildroot}/%{_pkgdocdir}; fi")
    print("if [ -d example ]; then cp -arf example %{buildroot}/%{_pkgdocdir}; fi")
    print("if [ -d examples ]; then cp -arf examples %{buildroot}/%{_pkgdocdir}; fi")
    print("pushd %{buildroot}")
    print("if [ -d usr/lib ]; then")
    print("\tfind usr/lib -type f -printf \"/%h/%f\\n\" >> filelist.lst")
    print("fi")
    print("if [ -d usr/lib64 ]; then")
    print("\tfind usr/lib64 -type f -printf \"/%h/%f\\n\" >> filelist.lst")
    print("fi")
    print("if [ -d usr/bin ]; then")
    print("\tfind usr/bin -type f -printf \"/%h/%f\\n\" >> filelist.lst")
    print("fi")
    print("if [ -d usr/sbin ]; then")
    print("\tfind usr/sbin -type f -printf \"/%h/%f\\n\" >> filelist.lst")
    print("fi")
470 471 472 473
    print("touch doclist.lst")
    print("if [ -d usr/share/man ]; then")
    print("\tfind usr/share/man -type f -printf \"/%h/%f.gz\\n\" >> doclist.lst")
    print("fi")
Z
Zhipeng Xie 已提交
474 475
    print("popd")
    print("mv %{buildroot}/filelist.lst .")
476
    print("mv %{buildroot}/doclist.lst .")
Z
Zhipeng Xie 已提交
477
    print("")
X
xw 已提交
478
    print("%files -n {name} -f filelist.lst".format(name=porter.get_pkg_name()))
Z
Zhipeng Xie 已提交
479

X
xw 已提交
480
    porter.prepare_pkg_files()
Z
Zhipeng Xie 已提交
481 482

    print("")
483
    print("%files help -f doclist.lst")
Z
Zhipeng Xie 已提交
484 485 486 487 488 489 490 491 492 493 494 495
    print("%{_pkgdocdir}")
    print("")
    print("%changelog")
    date_str = datetime.date.today().strftime("%a %b %d %Y")
    print("* {today} Python_Bot <Python_Bot@openeuler.org>".format(today=date_str))
    print("- Package Spec generated")

    sys.stdout = tmp

    return build_req_list


X
xw 已提交
496
def do_args(root):
Z
Zhipeng Xie 已提交
497 498 499
    parser = argparse.ArgumentParser()

    parser.add_argument("-s", "--spec", help="Create spec file", action="store_true")
500
    parser.add_argument("-R", "--requires", help="Get required python modules", action="store_true")
Z
Zhipeng Xie 已提交
501
    parser.add_argument("-b", "--build", help="Build rpm package", action="store_true")
502
    parser.add_argument("-B", "--buildinstall", help="Build&Install rpm package", action="store_true")
Z
Zhipeng Xie 已提交
503 504 505 506 507
    parser.add_argument("-r", "--rootpath", help="Build rpm package in root path", type=str, default=dft_root_path)
    parser.add_argument("-d", "--download", help="Download source file indicated path", action="store_true")
    parser.add_argument("-p", "--path", help="indicated path to store files", type=str, default=os.getcwd())
    parser.add_argument("-j", "--json", help="Get Package JSON info", action="store_true")
    parser.add_argument("-o", "--output", help="Output to file", type=str, default="")
X
xw 已提交
508
    parser.add_argument("-t", "--type", help="Build module type : python, perl...", type=str, default="python")
Z
Zhipeng Xie 已提交
509
    parser.add_argument("pkg", type=str, help="The Python Module Name")
X
xw 已提交
510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528

    return parser

def porter_creator(t_str, pkg):
    if (t_str == "python"):
        return PyPorter(pkg)
    elif (t_str == "perl"):
        return PerlPorter(pkg)

    return None



if __name__ == "__main__":

    dft_root_path=os.path.join(str(Path.home()))

    parser = do_args(dft_root_path)

Z
Zhipeng Xie 已提交
529 530
    args = parser.parse_args()

X
xw 已提交
531 532 533
    porter = porter_creator(args.type, args.pkg)
    if (porter is None):
        print("Type %s is not supported now\n" % args.type)
534 535
        sys.exit(1)

X
xw 已提交
536

537
    if (args.requires):
X
xw 已提交
538
        reqlist = porter.get_build_requires()
539 540 541
        if reqlist is not None:
            for req in reqlist:
                print(req)
X
xw 已提交
542
    elif (args.spec):
M
myeuler 已提交
543
        build_spec(porter, args.output)
X
xw 已提交
544
    elif (args.build):
M
myeuler 已提交
545
        ret = build_rpm(porter, args.rootpath)
Z
Zhipeng Xie 已提交
546
        if ret != "":
547 548
            print("build failed : BuildRequire : %s\n" % ret)
            sys.exit(1)
X
xw 已提交
549
    elif (args.buildinstall):
M
myeuler 已提交
550
        ret = build_install_rpm(porter, args.rootpath)
551 552 553
        if ret != "":
            print("Build & install failed\n")
            sys.exit(1)
X
xw 已提交
554
    elif (args.download):
M
myeuler 已提交
555
        download_source(porter, args.path)
X
xw 已提交
556 557
    elif (args.json):
        porter.store_json(args.path)
Z
Zhipeng Xie 已提交
558