euscan: shake the code
- add custom site handlers - use a custom user agent - fix some bugs in management commands Signed-off-by: Corentin Chary <corentincj@iksaif.net>
This commit is contained in:
parent
5634c59944
commit
752fb04425
8
AUTHORS
Normal file
8
AUTHORS
Normal file
@ -0,0 +1,8 @@
|
||||
* euscan
|
||||
Original author: Corentin Chary <corentin.chary@gmail.com>
|
||||
Current maintainer: Corentin Chary <corentin.chary@gmail.com>
|
||||
|
||||
* euscanwww
|
||||
Original author: Corentin Chary <corentin.chary@gmail.com>
|
||||
Current maintainer: Corentin Chary <corentin.chary@gmail.com>
|
||||
|
340
COPYING
Normal file
340
COPYING
Normal file
@ -0,0 +1,340 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Library General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) 19yy <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) 19yy name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Library General
|
||||
Public License instead of this License.
|
9
MANIFEST.in
Normal file
9
MANIFEST.in
Normal file
@ -0,0 +1,9 @@
|
||||
include AUTHORS
|
||||
include COPYING
|
||||
include NEWS
|
||||
include README
|
||||
include TODO
|
||||
include setup.py
|
||||
recursive-include bin *
|
||||
recursive-include man *
|
||||
recursive-include pym *.py
|
46
README
Normal file
46
README
Normal file
@ -0,0 +1,46 @@
|
||||
Package: gentoolkit/gentoolkit-dev
|
||||
Authors: Aron Griffis <agriffis@gentoo.org>
|
||||
Brandon Low <lostlogic@gentoo.org>
|
||||
Ian Leitch <port001@gentoo.org>
|
||||
Karl Trygve Kalleberg <karltk@gentoo.org>
|
||||
Marius Mauch <genone@gentoo.org>
|
||||
Paul Varner <fuzzyray@gentoo.org>
|
||||
See src/<tool>/AUTHORS for tool-specific authors
|
||||
|
||||
MOTIVATION
|
||||
|
||||
The gentoolkit and gentoolkit-dev packages contain a collection of useful
|
||||
administration scripts particular to the Gentoo Linux distribution. It contains
|
||||
rough drafts and implementations of features that may in time make it into
|
||||
Portage, or into full-fledged tools in their own right.
|
||||
|
||||
The gentoolkit-dev package is intended primarily for Gentoo developers.
|
||||
|
||||
CONTENTS
|
||||
|
||||
gentoolkit
|
||||
==========
|
||||
eclean - tool to clean up outdated distfiles and packages
|
||||
equery - replacement for etcat and qpkg
|
||||
etcat - extracts auxillary information from portage (deprecated)
|
||||
euse - tool to manage USE flags
|
||||
glsa-check - tool to manage GLSA's (Gentoo Linux Security Advisory)
|
||||
qpkg - convient package query tool (deprecated)
|
||||
revdep-rebuild - scans/fixes broken shared libs and binaries
|
||||
|
||||
gentoolkit-dev
|
||||
==============
|
||||
ebump - Ebuild revision bumper
|
||||
echangelog - update portage ChangeLogs
|
||||
ego -
|
||||
ekeyword - modify package KEYWORDS
|
||||
epkgmove - tool for moving and renaming packages in CVS
|
||||
eviewcvs - generate viewcvs URLs
|
||||
gensync - Overlay Sync Tool
|
||||
|
||||
IMPROVEMENTS
|
||||
|
||||
Any suggestions for improvements should be sent to tools-portage@gentoo.org, or
|
||||
added as a bug assigned to us.
|
||||
|
||||
We only accept new contributions if they are written in bash or python.
|
2
TODO
2
TODO
@ -14,8 +14,6 @@ euscan
|
||||
Site Handlers
|
||||
-------------
|
||||
|
||||
- python: PyPi
|
||||
- PHP: PECL / PEAR
|
||||
- ftp.kde.org: doesn't scan the "unstable" tree
|
||||
- mysql: should use http://downloads.mysql.com/archives/
|
||||
- mariadb: should use http://downloads.askmonty.org/MariaDB/+releases/
|
||||
|
220
bin/euscan
Executable file
220
bin/euscan
Executable file
@ -0,0 +1,220 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
"""Copyright 2011 Gentoo Foundation
|
||||
Distributed under the terms of the GNU General Public License v2
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
""" Meta """
|
||||
__author__ = "Corentin Chary (iksaif)"
|
||||
__email__ = "corentin.chary@gmail.com"
|
||||
__version__ = "git"
|
||||
__productname__ = "euscan"
|
||||
__description__ = "A tool to detect new upstream releases."
|
||||
|
||||
""" Imports """
|
||||
|
||||
import os
|
||||
import sys
|
||||
import getopt
|
||||
import errno
|
||||
|
||||
from portage.output import white, yellow, turquoise, green, EOutput
|
||||
|
||||
import gentoolkit.pprinter as pp
|
||||
from gentoolkit.eclean.search import (port_settings)
|
||||
|
||||
from euscan import CONFIG, output
|
||||
from euscan.scan import scan_upstream
|
||||
|
||||
""" Globals """
|
||||
|
||||
def setupSignals():
|
||||
""" This block ensures that ^C interrupts are handled quietly. """
|
||||
import signal
|
||||
|
||||
def exithandler(signum,frame):
|
||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
signal.signal(signal.SIGTERM, signal.SIG_IGN)
|
||||
print ()
|
||||
sys.exit(errno.EINTR)
|
||||
|
||||
signal.signal(signal.SIGINT, exithandler)
|
||||
signal.signal(signal.SIGTERM, exithandler)
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
|
||||
|
||||
def printVersion():
|
||||
"""Output the version info."""
|
||||
print( "%s (%s) - %s" \
|
||||
% (__productname__, __version__, __description__))
|
||||
print()
|
||||
print("Author: %s <%s>" % (__author__,__email__))
|
||||
print("Copyright 2011 Gentoo Foundation")
|
||||
print("Distributed under the terms of the GNU General Public License v2")
|
||||
|
||||
|
||||
def printUsage(_error=None, help=None):
|
||||
"""Print help message. May also print partial help to stderr if an
|
||||
error from {'options'} is specified."""
|
||||
|
||||
out = sys.stdout
|
||||
if _error:
|
||||
out = sys.stderr
|
||||
if not _error in ('global-options', 'packages',):
|
||||
_error = None
|
||||
if not _error and not help: help = 'all'
|
||||
if _error in ('global-options',):
|
||||
print( pp.error("Wrong option on command line."), file=out)
|
||||
print( file=out)
|
||||
if _error in ('packages',):
|
||||
print( pp.error("You need to specify exactly one package."), file=out)
|
||||
print( file=out)
|
||||
print( white("Usage:"), file=out)
|
||||
if _error in ('global-options', 'packages',) or help == 'all':
|
||||
print( " "+turquoise(__productname__),
|
||||
yellow("[options]"),
|
||||
green("<package>"), file=out)
|
||||
if _error in ('global-options',) or help == 'all':
|
||||
print( " "+turquoise(__productname__),
|
||||
yellow("[--help, --version]"), file=out)
|
||||
|
||||
print(file=out)
|
||||
if _error in ('global-options',) or help:
|
||||
print( "Available ", yellow("options")+":", file=out)
|
||||
print( yellow(" -C, --nocolor")+
|
||||
" - turn off colors on output", file=out)
|
||||
print( yellow(" -q, --quiet")+
|
||||
" - be as quiet as possible", file=out)
|
||||
print( yellow(" -h, --help")+ \
|
||||
" - display the help screen", file=out)
|
||||
print( yellow(" -V, --version")+
|
||||
" - display version info", file=out)
|
||||
print( file=out)
|
||||
print( yellow(" -1, --oneshot")+
|
||||
" - stop as soon as a new version is found", file=out)
|
||||
print( yellow(" -b, --brute-force=<level>")+
|
||||
" - define the brute force "+yellow("<level>")+" (default: 2)\n" +
|
||||
" " * 29 + "bigger levels will generate more versions numbers\n" +
|
||||
" " * 29 + "0 means disabled", file=out)
|
||||
print( file=out)
|
||||
if _error in ('packages',) or help:
|
||||
print( green(" package")+
|
||||
" - the package (or ebuild) you want to scan", file=out)
|
||||
print( file=out)
|
||||
'''print( "More detailed instruction can be found in",
|
||||
turquoise("`man %s`" % __productname__), file=out)'''
|
||||
|
||||
|
||||
class ParseArgsException(Exception):
|
||||
"""For parseArgs() -> main() communications."""
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
def __str__(self):
|
||||
return repr(self.value)
|
||||
|
||||
|
||||
def parseArgs():
|
||||
"""Parse the command line arguments. Raise exceptions on
|
||||
errors. Returns package and affect the CONFIG dict.
|
||||
"""
|
||||
|
||||
def optionSwitch(opts):
|
||||
"""local function for interpreting command line options
|
||||
and setting options accordingly"""
|
||||
return_code = True
|
||||
for o, a in opts:
|
||||
if o in ("-h", "--help"):
|
||||
raise ParseArgsException('help')
|
||||
elif o in ("-V", "--version"):
|
||||
raise ParseArgsException('version')
|
||||
elif o in ("-C", "--nocolor"):
|
||||
CONFIG['nocolor'] = True
|
||||
pp.output.nocolor()
|
||||
elif o in ("-q", "--quiet"):
|
||||
CONFIG['quiet'] = True
|
||||
CONFIG['verbose'] = False
|
||||
elif o in ("-1", "--oneshot"):
|
||||
CONFIG['oneshot'] = True
|
||||
elif o in ("-b", "--brute-force"):
|
||||
CONFIG['brute-force'] = int(a)
|
||||
elif o in ("-v", "--verbose") and not CONFIG['quiet']:
|
||||
CONFIG['verbose'] = True
|
||||
else:
|
||||
return_code = False
|
||||
|
||||
return return_code
|
||||
|
||||
' here are the different allowed command line options (getopt args) '
|
||||
getopt_options = {'short':{}, 'long':{}}
|
||||
getopt_options['short']['global'] = "hVCqv1b:"
|
||||
getopt_options['long']['global'] = ["help", "version", "nocolor", "quiet",
|
||||
"verbose", "oneshot", "brute-force="]
|
||||
|
||||
short_opts = getopt_options['short']['global']
|
||||
long_opts = getopt_options['long']['global']
|
||||
opts_mode = 'global'
|
||||
|
||||
' apply getopts to command line, show partial help on failure '
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], short_opts, long_opts)
|
||||
except:
|
||||
raise ParseArgsException(opts_mode+'-options')
|
||||
|
||||
' set options accordingly '
|
||||
optionSwitch(opts)
|
||||
|
||||
if len(args) != 1:
|
||||
raise ParseArgsException('packages')
|
||||
|
||||
return args[0]
|
||||
|
||||
def main():
|
||||
"""Parse command line and execute all actions."""
|
||||
CONFIG['nocolor'] = (port_settings["NOCOLOR"] in ('yes','true')
|
||||
or not sys.stdout.isatty())
|
||||
if CONFIG['nocolor']:
|
||||
pp.output.nocolor()
|
||||
' parse command line options and actions '
|
||||
try:
|
||||
package = parseArgs()
|
||||
except ParseArgsException as e:
|
||||
if e.value == 'help':
|
||||
printUsage(help='all')
|
||||
sys.exit(0)
|
||||
elif e.value[:5] == 'help-':
|
||||
printUsage(help=e.value[5:])
|
||||
sys.exit(0)
|
||||
elif e.value == 'version':
|
||||
printVersion()
|
||||
sys.exit(0)
|
||||
else:
|
||||
printUsage(e.value)
|
||||
sys.exit(errno.EINVAL)
|
||||
|
||||
""" Change euscan's output """
|
||||
output = EOutput(CONFIG['quiet'])
|
||||
ret = scan_upstream(package)
|
||||
|
||||
print ()
|
||||
|
||||
for url, version in ret:
|
||||
print ("Upstream Version: "
|
||||
+ pp.number("%s" % version)
|
||||
+ pp.path(" %s" % url))
|
||||
|
||||
if not len(ret):
|
||||
print (pp.warn("Didn't find any new version, "
|
||||
+ "check package's homepage for "
|
||||
+ "more informations"));
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
setupSignals()
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print( "Aborted.")
|
||||
sys.exit(errno.EINTR)
|
||||
sys.exit(0)
|
825
euscan
825
euscan
@ -1,825 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
"""Copyright 2011 Gentoo Foundation
|
||||
Distributed under the terms of the GNU General Public License v2
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
# Meta:
|
||||
__author__ = "Corentin Chary (iksaif)"
|
||||
__email__ = "corentin.chary@gmail.com"
|
||||
__version__ = "git"
|
||||
__productname__ = "euscan"
|
||||
__description__ = "A tool to detect new upstream releases."
|
||||
|
||||
# =======
|
||||
# Imports
|
||||
# =======
|
||||
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import time
|
||||
import getopt
|
||||
import errno
|
||||
import random
|
||||
import urllib2
|
||||
import StringIO
|
||||
|
||||
import pkg_resources
|
||||
|
||||
import portage
|
||||
import portage.versions
|
||||
from portage import dep
|
||||
from portage.dbapi import porttree
|
||||
from portage.output import white, yellow, turquoise, green, teal, red, EOutput
|
||||
|
||||
import gentoolkit.pprinter as pp
|
||||
from gentoolkit import errors
|
||||
from gentoolkit.query import Query
|
||||
from gentoolkit.eclean.search import (port_settings)
|
||||
|
||||
# =======
|
||||
# Globals
|
||||
# =======
|
||||
|
||||
QUERY_OPTS = {"include_masked": True}
|
||||
|
||||
BLACKLIST_VERSIONS = [
|
||||
# Compatibility package for running binaries linked against a pre gcc 3.4 libstdc++, won't be updated
|
||||
'>=sys-libs/libstdc++-v3-3.4',
|
||||
]
|
||||
|
||||
BLACKLIST_PACKAGES = [
|
||||
# These kernels are almost dead
|
||||
'sys-kernel/usermode-sources',
|
||||
'sys-kernel/xbox-sources',
|
||||
'sys-kernel/cell-sources',
|
||||
]
|
||||
|
||||
SCANDIR_BLACKLIST_URLS = [
|
||||
'mirror://rubygems/(.*)', # Not browsable
|
||||
'mirror://gentoo/(.*)' # Directory too big
|
||||
]
|
||||
|
||||
BRUTEFORCE_BLACKLIST_PACKAGES = [
|
||||
'net-zope/plonepopoll' # infinite loop any http://plone.org/products/plonepopoll/releases/*/plonepopoll-2-6-1.tgz link will work
|
||||
]
|
||||
|
||||
BRUTEFORCE_BLACKLIST_URLS = [
|
||||
'http://(.*)dockapps.org/download.php/id/(.*)', # infinite loop
|
||||
'http://hydra.nixos.org/build/(.*)', # infinite loop
|
||||
'http://www.rennings.net/gentoo/distfiles/(.*)' # Doesn't respect 404, infinite loop
|
||||
]
|
||||
|
||||
def htop_vercmp(a, b):
|
||||
def fixver(v):
|
||||
if v in ['0.11', '0.12', '0.13']:
|
||||
v = '0.1.' + v[3:]
|
||||
return v
|
||||
|
||||
return simple_vercmp(fixver(a), fixver(b))
|
||||
|
||||
VERSION_CMP_PACKAGE_QUIRKS = {
|
||||
'sys-process/htop' : htop_vercmp
|
||||
}
|
||||
|
||||
_v = r'((\d+)((\.\d+)*)([a-zA-Z]*?)(((-|_)(pre|p|beta|b|alpha|a|rc|r)\d*)*))'
|
||||
|
||||
# =========
|
||||
# Functions
|
||||
# =========
|
||||
|
||||
def cast_int_components(version):
|
||||
for i, obj in enumerate(version):
|
||||
try:
|
||||
version[i] = int(obj)
|
||||
except ValueError:
|
||||
pass
|
||||
return version
|
||||
|
||||
def simple_vercmp(a, b):
|
||||
if a == b:
|
||||
return 0
|
||||
|
||||
# For sane versions
|
||||
r = portage.versions.vercmp(a, b)
|
||||
|
||||
if r is not None:
|
||||
return r
|
||||
|
||||
# Fallback
|
||||
a = pkg_resources.parse_version(a)
|
||||
b = pkg_resources.parse_version(b)
|
||||
|
||||
if a < b:
|
||||
return -1
|
||||
else:
|
||||
return 1
|
||||
|
||||
def vercmp(package, a, b):
|
||||
if package in VERSION_CMP_PACKAGE_QUIRKS:
|
||||
return VERSION_CMP_PACKAGE_QUIRKS[package](a, b)
|
||||
return simple_vercmp(a, b)
|
||||
|
||||
def skipnightly(a, b):
|
||||
a = pkg_resources.parse_version(a)
|
||||
b = pkg_resources.parse_version(b)
|
||||
|
||||
# Try to skip nightly builds when not wanted (www-apps/moodle)
|
||||
if len(a) != len(b) and len(b) == 2 and len(b[0]) == len('yyyymmdd'):
|
||||
return True
|
||||
return False
|
||||
|
||||
def generate_templates_vars(version):
|
||||
ret = []
|
||||
|
||||
part = split_version(version)
|
||||
for i in range(2, len(part)):
|
||||
ver = []
|
||||
var = []
|
||||
for j in range(i):
|
||||
ver.append(str(part[j]))
|
||||
var.append('${%d}' % j)
|
||||
|
||||
ret.append((".".join(ver), ".".join(var)))
|
||||
ret.append((version, '${PV}'))
|
||||
ret.reverse()
|
||||
return ret
|
||||
|
||||
def template_from_url(url, version):
|
||||
prefix, chunks = url.split('://')
|
||||
chunks = chunks.split('/')
|
||||
|
||||
for i in range(len(chunks)):
|
||||
chunk = chunks[i]
|
||||
|
||||
subs = generate_templates_vars(version)
|
||||
for sub in subs:
|
||||
chunk = chunk.replace(sub[0], sub[1])
|
||||
|
||||
chunks[i] = chunk
|
||||
|
||||
|
||||
return prefix + "://" + "/".join(chunks)
|
||||
|
||||
def url_from_template(url, version):
|
||||
components = split_version(version)
|
||||
|
||||
url = url.replace('${PV}', version)
|
||||
for i in range(len(components)):
|
||||
url = url.replace('${%d}' % i, str(components[i]))
|
||||
|
||||
return url
|
||||
|
||||
# Stolen from distutils.LooseVersion
|
||||
# Used for brute force to increment the version
|
||||
def split_version(version):
|
||||
component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE)
|
||||
components = filter(lambda x: x and x != '.', component_re.split(version))
|
||||
for i in range(len(components)):
|
||||
try:
|
||||
components[i] = int(components[i])
|
||||
except ValueError:
|
||||
pass
|
||||
return components
|
||||
|
||||
def join_version(components):
|
||||
version = ""
|
||||
for i in range(len(components)):
|
||||
version += str(components[i])
|
||||
if i >= len(components) - 1:
|
||||
break
|
||||
if type(components[i]) != str and type(components[i + 1]) != str:
|
||||
version += "."
|
||||
return version
|
||||
|
||||
def increment_version(components, level):
|
||||
n = len(components)
|
||||
|
||||
if level > n - 1 or level < 0:
|
||||
raise Exception
|
||||
|
||||
for i in range(n, level + 1, -1):
|
||||
if type(components[i - 1]) == int:
|
||||
components[i - 1] = 0
|
||||
|
||||
if type(components[level]) == int:
|
||||
components[level] += 1
|
||||
|
||||
return components
|
||||
|
||||
def gen_versions(components, level):
|
||||
n = len(components)
|
||||
depth = level
|
||||
level = min(level, n)
|
||||
|
||||
if not n:
|
||||
return []
|
||||
|
||||
versions = []
|
||||
|
||||
for i in range(n, n - level, -1):
|
||||
increment_version(components, i - 1)
|
||||
for j in range(depth):
|
||||
versions.append(list(components))
|
||||
increment_version(components, i - 1)
|
||||
|
||||
return versions
|
||||
|
||||
def tryurl(fileurl, output, template):
|
||||
result = True
|
||||
|
||||
output.ebegin("Trying: " + fileurl)
|
||||
|
||||
try:
|
||||
basename = os.path.basename(fileurl)
|
||||
|
||||
fp = urllib2.urlopen(fileurl, None, 5)
|
||||
headers = fp.info()
|
||||
|
||||
if 'Content-disposition' in headers and basename not in headers['Content-disposition']:
|
||||
result = None
|
||||
elif 'Content-Length' in headers and headers['Content-Length'] == '0':
|
||||
result = None
|
||||
elif 'text/html' in headers['Content-Type']:
|
||||
result = None
|
||||
elif fp.geturl() != fileurl:
|
||||
regex = regex_from_template(template)
|
||||
baseregex = regex_from_template(os.path.basename(template))
|
||||
basename2 = os.path.basename(fp.geturl())
|
||||
|
||||
# Redirect to another (earlier?) version
|
||||
if basename != basename2 and (re.match(regex, fp.geturl()) or re.match(baseregex, basename2)):
|
||||
result = None
|
||||
|
||||
|
||||
if result:
|
||||
result = (fp.geturl(), fp.info())
|
||||
|
||||
except urllib2.URLError:
|
||||
result = None
|
||||
except IOError:
|
||||
result = None
|
||||
|
||||
output.eend(errno.ENOENT if not result else 0)
|
||||
|
||||
return result
|
||||
|
||||
def regex_from_template(template):
|
||||
template = re.escape(template)
|
||||
template = template.replace('\$\{', '${')
|
||||
template = template.replace('\}', '}')
|
||||
template = template.replace('}\.$', '}.$')
|
||||
template = template.replace('${1}', r'([\d]+?)')
|
||||
template = re.sub(r'(\$\{\d+\}\.?)+', r'([\w]+?)', template)
|
||||
#template = re.sub(r'(\$\{\d+\}\.?)+', r'([\w]+?)', template)
|
||||
#template = re.sub(r'(\$\{\d+\}\.+)+', '(.+?)\.', template)
|
||||
#template = re.sub(r'(\$\{\d+\})+', '(.+?)', template)
|
||||
template = template.replace('${PV}', _v)
|
||||
template = template + r'/?$'
|
||||
return template
|
||||
|
||||
def basedir_from_template(template):
|
||||
idx = template.find('${')
|
||||
if idx == -1:
|
||||
return template
|
||||
|
||||
idx = template[0:idx].rfind('/')
|
||||
if idx == -1:
|
||||
return ""
|
||||
|
||||
return template[0:idx]
|
||||
|
||||
def generate_scan_paths(url):
|
||||
prefix, chunks = url.split('://')
|
||||
chunks = chunks.split('/')
|
||||
|
||||
steps = []
|
||||
|
||||
path = prefix + ":/"
|
||||
for chunk in chunks:
|
||||
if '${' in chunk:
|
||||
steps.append((path, regex_from_template(chunk)))
|
||||
path = ""
|
||||
else:
|
||||
path += "/"
|
||||
path += chunk
|
||||
return steps
|
||||
|
||||
def versionBlacklisted(cp, version, output=None):
|
||||
rule = None
|
||||
cpv = '%s-%s' % (cp, version)
|
||||
|
||||
for bv in BLACKLIST_VERSIONS:
|
||||
if dep.match_from_list(bv, [cpv]):
|
||||
rule = bv
|
||||
None
|
||||
|
||||
if rule and output:
|
||||
output.einfo("%s is blacklisted by rule %s" % (cpv, bv))
|
||||
return rule is not None
|
||||
|
||||
def scan_directory_recursive(cpv, url, steps, vmin, vmax, output):
|
||||
if not steps:
|
||||
return []
|
||||
|
||||
cp, ver, rev = portage.pkgsplit(cpv)
|
||||
url += steps[0][0]
|
||||
pattern = steps[0][1]
|
||||
|
||||
steps = steps[1:]
|
||||
|
||||
output.einfo("Scanning: %s" % url)
|
||||
|
||||
try:
|
||||
fp = urllib2.urlopen(url, None, 5)
|
||||
except urllib2.URLError:
|
||||
return []
|
||||
except IOError:
|
||||
return []
|
||||
|
||||
data = fp.read()
|
||||
|
||||
results = []
|
||||
|
||||
if re.search("<\s*a\s+[^>]*href", data):
|
||||
from BeautifulSoup import BeautifulSoup
|
||||
|
||||
soup = BeautifulSoup(data)
|
||||
|
||||
for link in soup.findAll('a'):
|
||||
href = link.get("href")
|
||||
if not href:
|
||||
continue
|
||||
if href.startswith(url):
|
||||
href = href.replace(url, "", 1)
|
||||
|
||||
match = re.match(pattern, href, re.I)
|
||||
if match:
|
||||
results.append((match.group(1), match.group(0)))
|
||||
|
||||
elif url.startswith('ftp://'): # Probably a FTP Server
|
||||
buf = StringIO.StringIO(data)
|
||||
for line in buf.readlines():
|
||||
line = line.replace("\n", "").replace("\r", "")
|
||||
match = re.search(pattern, line, re.I)
|
||||
if match:
|
||||
results.append((match.group(1), match.group(0)))
|
||||
# add url
|
||||
|
||||
versions = []
|
||||
|
||||
for version, path in results:
|
||||
if vmin and vercmp(cp, version, vmin) <= 0:
|
||||
continue
|
||||
if vmax and vercmp(cp, version, vmax) >= 0:
|
||||
continue
|
||||
|
||||
if versionBlacklisted(cp, version, output):
|
||||
continue
|
||||
|
||||
if skipnightly(vmin, version):
|
||||
continue
|
||||
|
||||
if not url.endswith('/') and not path.startswith('/'):
|
||||
path = url + '/' + path
|
||||
else:
|
||||
path = url + path
|
||||
|
||||
versions.append((path, version))
|
||||
if steps:
|
||||
ret = scan_directory_recursive(cpv, path, steps, vmin, vmax, output)
|
||||
versions.extend(ret)
|
||||
return versions
|
||||
|
||||
'''
|
||||
- python: PyPi
|
||||
- PHP: PECL / PEAR
|
||||
- ftp.kde.org: doesn't scan the "unstable" tree
|
||||
- mysql: should use http://downloads.mysql.com/archives/
|
||||
- mariadb: should use http://downloads.askmonty.org/MariaDB/+releases/
|
||||
'''
|
||||
|
||||
def scan_directory(cpv, url, options, output, limit=None):
|
||||
# Ftp: list dir
|
||||
# Handle mirrors
|
||||
if not options["scan-dir"]:
|
||||
return []
|
||||
|
||||
for bu in SCANDIR_BLACKLIST_URLS:
|
||||
if re.match(bu, url):
|
||||
output.einfo("%s is blacklisted by rule %s" % (url, bu))
|
||||
return []
|
||||
|
||||
resolved_url = parseMirror(url, output)
|
||||
|
||||
catpkg, ver, rev = portage.pkgsplit(cpv)
|
||||
|
||||
template = template_from_url(resolved_url, ver)
|
||||
if '${' not in template:
|
||||
output.einfo("Url doesn't seems to depend on version: %s not found in %s"
|
||||
% (ver, fileurl))
|
||||
return []
|
||||
else:
|
||||
output.einfo("Scanning: %s" % template)
|
||||
|
||||
steps = generate_scan_paths(template)
|
||||
return scan_directory_recursive(cpv, "", steps, ver, limit, output)
|
||||
|
||||
def brute_force(cpv, fileurl, options, output, limit=None):
|
||||
if options["brute-force"] <= 0:
|
||||
return []
|
||||
|
||||
catpkg, ver, rev = portage.pkgsplit(cpv)
|
||||
|
||||
for bp in BRUTEFORCE_BLACKLIST_PACKAGES:
|
||||
if re.match(bp, catpkg):
|
||||
output.einfo("%s is blacklisted by rule %s" % (catpkg, bp))
|
||||
return []
|
||||
|
||||
for bp in BRUTEFORCE_BLACKLIST_URLS:
|
||||
if re.match(bp, fileurl):
|
||||
output.einfo("%s is blacklisted by rule %s" % (catpkg, bp))
|
||||
return []
|
||||
|
||||
output.einfo("Generating version from " + ver)
|
||||
|
||||
components = split_version(ver)
|
||||
versions = gen_versions(components, options["brute-force"])
|
||||
|
||||
|
||||
""" Remove unwanted versions """
|
||||
for v in versions:
|
||||
if vercmp(catpkg, ver, join_version(v)) >= 0:
|
||||
versions.remove(v)
|
||||
|
||||
if not versions:
|
||||
output.einfo("Can't generate new versions from " + ver)
|
||||
return []
|
||||
|
||||
template = template_from_url(fileurl, ver)
|
||||
|
||||
if '${PV}' not in template:
|
||||
output.einfo("Url doesn't seems to depend on full version: %s not found in %s"
|
||||
% (ver, fileurl))
|
||||
return []
|
||||
else:
|
||||
output.einfo("Brute forcing: %s" % template)
|
||||
|
||||
result = []
|
||||
|
||||
i = 0
|
||||
done = []
|
||||
|
||||
while i < len(versions):
|
||||
components = versions[i]
|
||||
i += 1
|
||||
if components in done:
|
||||
continue
|
||||
done.append(tuple(components))
|
||||
|
||||
vstring = join_version(components)
|
||||
|
||||
if versionBlacklisted(catpkg, vstring, output):
|
||||
continue
|
||||
|
||||
if limit and vercmp(catpkg, vstring, limit) >= 0:
|
||||
continue
|
||||
|
||||
url = url_from_template(template, vstring)
|
||||
|
||||
infos = tryurl(url, output, template)
|
||||
|
||||
if not infos:
|
||||
continue
|
||||
|
||||
result.append([url, vstring])
|
||||
|
||||
if options["brute-force-recursive"]:
|
||||
for v in gen_versions(components, options["brute-force"]):
|
||||
if v not in versions and tuple(v) not in done:
|
||||
versions.append(v)
|
||||
|
||||
if options["oneshot"]:
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def parseMirror(uri, output):
|
||||
from random import shuffle
|
||||
|
||||
mirrors = portage.settings.thirdpartymirrors()
|
||||
|
||||
if not uri.startswith("mirror://"):
|
||||
return uri
|
||||
|
||||
eidx = uri.find("/", 9)
|
||||
if eidx == -1:
|
||||
output.einfo("Invalid mirror definition in SRC_URI:\n")
|
||||
output.einfo(" %s\n" % (uri))
|
||||
return None
|
||||
|
||||
mirrorname = uri[9:eidx]
|
||||
path = uri[eidx+1:]
|
||||
|
||||
if mirrorname in mirrors:
|
||||
mirrors = mirrors[mirrorname]
|
||||
shuffle(mirrors)
|
||||
uri = mirrors[0].strip("/") + "/" + path
|
||||
else:
|
||||
output.einfo("No known mirror by the name: %s\n" % (mirrorname))
|
||||
return None
|
||||
|
||||
return uri
|
||||
|
||||
def setupSignals():
|
||||
""" This block ensures that ^C interrupts are handled quietly. """
|
||||
import signal
|
||||
|
||||
def exithandler(signum,frame):
|
||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
signal.signal(signal.SIGTERM, signal.SIG_IGN)
|
||||
print ()
|
||||
sys.exit(errno.EINTR)
|
||||
|
||||
signal.signal(signal.SIGINT, exithandler)
|
||||
signal.signal(signal.SIGTERM, exithandler)
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
|
||||
|
||||
def printVersion():
|
||||
"""Output the version info."""
|
||||
print( "%s (%s) - %s" \
|
||||
% (__productname__, __version__, __description__))
|
||||
print()
|
||||
print("Author: %s <%s>" % (__author__,__email__))
|
||||
print("Copyright 2011 Gentoo Foundation")
|
||||
print("Distributed under the terms of the GNU General Public License v2")
|
||||
|
||||
|
||||
def printUsage(_error=None, help=None):
|
||||
"""Print help message. May also print partial help to stderr if an
|
||||
error from {'options'} is specified."""
|
||||
|
||||
out = sys.stdout
|
||||
if _error:
|
||||
out = sys.stderr
|
||||
if not _error in ('global-options', 'packages',):
|
||||
_error = None
|
||||
if not _error and not help: help = 'all'
|
||||
if _error in ('global-options',):
|
||||
print( pp.error("Wrong option on command line."), file=out)
|
||||
print( file=out)
|
||||
if _error in ('packages',):
|
||||
print( pp.error("You need to specify exactly one package."), file=out)
|
||||
print( file=out)
|
||||
print( white("Usage:"), file=out)
|
||||
if _error in ('global-options', 'packages',) or help == 'all':
|
||||
print( " "+turquoise(__productname__),
|
||||
yellow("[options]"),
|
||||
green("<package>"), file=out)
|
||||
if _error in ('global-options',) or help == 'all':
|
||||
print( " "+turquoise(__productname__),
|
||||
yellow("[--help, --version]"), file=out)
|
||||
|
||||
print(file=out)
|
||||
if _error in ('global-options',) or help:
|
||||
print( "Available ", yellow("options")+":", file=out)
|
||||
print( yellow(" -C, --nocolor")+
|
||||
" - turn off colors on output", file=out)
|
||||
print( yellow(" -q, --quiet")+
|
||||
" - be as quiet as possible", file=out)
|
||||
print( yellow(" -h, --help")+ \
|
||||
" - display the help screen", file=out)
|
||||
print( yellow(" -V, --version")+
|
||||
" - display version info", file=out)
|
||||
print( file=out)
|
||||
print( yellow(" -1, --oneshot")+
|
||||
" - stop as soon as a new version is found", file=out)
|
||||
print( yellow(" -b, --brute-force=<level>")+
|
||||
" - define the brute force "+yellow("<level>")+" (default: 2)\n" +
|
||||
" " * 29 + "bigger levels will generate more versions numbers\n" +
|
||||
" " * 29 + "0 means disabled", file=out)
|
||||
print( file=out)
|
||||
if _error in ('packages',) or help:
|
||||
print( green(" package")+
|
||||
" - the package (or ebuild) you want to scan", file=out)
|
||||
print( file=out)
|
||||
#print( "More detailed instruction can be found in",
|
||||
# turquoise("`man %s`" % __productname__), file=out)
|
||||
|
||||
|
||||
class ParseArgsException(Exception):
|
||||
"""For parseArgs() -> main() communications."""
|
||||
def __init__(self, value):
|
||||
self.value = value # sdfgsdfsdfsd
|
||||
def __str__(self):
|
||||
return repr(self.value)
|
||||
|
||||
|
||||
def parseArgs(options={}):
|
||||
"""Parse the command line arguments. Raise exceptions on
|
||||
errors. Returns package and affect the options dict.
|
||||
"""
|
||||
|
||||
def optionSwitch(option,opts):
|
||||
"""local function for interpreting command line options
|
||||
and setting options accordingly"""
|
||||
return_code = True
|
||||
for o, a in opts:
|
||||
if o in ("-h", "--help"):
|
||||
raise ParseArgsException('help')
|
||||
elif o in ("-V", "--version"):
|
||||
raise ParseArgsException('version')
|
||||
elif o in ("-C", "--nocolor"):
|
||||
options['nocolor'] = True
|
||||
pp.output.nocolor()
|
||||
elif o in ("-q", "--quiet"):
|
||||
options['quiet'] = True
|
||||
options['verbose'] = False
|
||||
elif o in ("-1", "--oneshot"):
|
||||
options['oneshot'] = True
|
||||
elif o in ("-b", "--brute-force"):
|
||||
options['brute-force'] = int(a)
|
||||
elif o in ("-v", "--verbose") and not options['quiet']:
|
||||
options['verbose'] = True
|
||||
else:
|
||||
return_code = False
|
||||
|
||||
return return_code
|
||||
|
||||
# here are the different allowed command line options (getopt args)
|
||||
getopt_options = {'short':{}, 'long':{}}
|
||||
getopt_options['short']['global'] = "hVCqv1b:"
|
||||
getopt_options['long']['global'] = ["help", "version", "nocolor", "quiet",
|
||||
"verbose", "oneshot", "brute-force="]
|
||||
# set default options, except 'nocolor', which is set in main()
|
||||
options['quiet'] = False
|
||||
options['verbose'] = False
|
||||
options['brute-force'] = 2
|
||||
options['oneshot'] = False
|
||||
options['brute-force-recursive'] = True # FIXME add an option
|
||||
options['scan-dir'] = True # FIXME add an option
|
||||
|
||||
short_opts = getopt_options['short']['global']
|
||||
long_opts = getopt_options['long']['global']
|
||||
opts_mode = 'global'
|
||||
|
||||
# apply getopts to command line, show partial help on failure
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], short_opts, long_opts)
|
||||
except:
|
||||
raise ParseArgsException(opts_mode+'-options')
|
||||
|
||||
# set options accordingly
|
||||
optionSwitch(options,opts)
|
||||
|
||||
if len(args) != 1:
|
||||
raise ParseArgsException('packages')
|
||||
|
||||
return args[0]
|
||||
|
||||
def scanUpstream(options, package, output):
|
||||
matches = Query(package).find(
|
||||
include_masked=QUERY_OPTS['include_masked'],
|
||||
in_installed=False
|
||||
)
|
||||
|
||||
if not matches:
|
||||
sys.stderr.write(pp.warn("No package matching '%s'" % pp.pkgquery(package)))
|
||||
sys.exit(errno.ENOENT)
|
||||
|
||||
matches = sorted(matches)
|
||||
pkg = matches.pop()
|
||||
|
||||
if '9999' in pkg.version:
|
||||
if len(matches) == 0:
|
||||
sys.stderr.write(pp.warn("Package '%s' only have a dev version (9999)" % pp.pkgquery(package)))
|
||||
sys.exit(errno.ENOENT)
|
||||
else:
|
||||
pkg = matches.pop()
|
||||
|
||||
if pkg.cp in BLACKLIST_PACKAGES:
|
||||
sys.stderr.write(pp.warn("Package '%s' is blacklisted" % pp.pkgquery(package)))
|
||||
sys.exit(errno.ENOENT)
|
||||
|
||||
pp.uprint(" * %s [%s]" % (pp.cpv(pkg.cpv), pp.section(pkg.repo_name())))
|
||||
pp.uprint()
|
||||
|
||||
ebuild_path = pkg.ebuild_path()
|
||||
if ebuild_path:
|
||||
pp.uprint('Ebuild: ' + pp.path(os.path.normpath(ebuild_path)))
|
||||
|
||||
pp.uprint('Repository: ' + pkg.repo_name())
|
||||
pp.uprint('Homepage: ' + pkg.environment("HOMEPAGE"))
|
||||
pp.uprint('Description: ' + pkg.environment("DESCRIPTION"))
|
||||
|
||||
cpv = pkg.cpv
|
||||
metadata = {
|
||||
"EAPI" : port_settings["EAPI"],
|
||||
"SRC_URI" : pkg.environment("SRC_URI", False),
|
||||
}
|
||||
use = frozenset(port_settings["PORTAGE_USE"].split())
|
||||
try:
|
||||
alist = porttree._parse_uri_map(cpv, metadata, use=use)
|
||||
aalist = porttree._parse_uri_map(cpv, metadata)
|
||||
except InvalidDependString as e:
|
||||
sys.stderr.write(pp.warn("%s\n" % str(e)))
|
||||
sys.stderr.write(pp.warn("Invalid SRC_URI for '%s'" % pp.pkgquery(cpv)))
|
||||
sys.exit(errno.ENOENT)
|
||||
|
||||
if "mirror" in portage.settings.features:
|
||||
fetchme = aalist
|
||||
else:
|
||||
fetchme = alist
|
||||
|
||||
versions = []
|
||||
|
||||
for filename in fetchme:
|
||||
for url in fetchme[filename]:
|
||||
print ()
|
||||
output.einfo("SRC_URI is '%s'" % url)
|
||||
|
||||
if '://' not in url:
|
||||
output.einfo("Invalid url '%s'" % url)
|
||||
continue
|
||||
|
||||
''' Try normal scan '''
|
||||
versions.extend(scan_directory(cpv, url, options, output))
|
||||
|
||||
if versions and options['oneshot']:
|
||||
break
|
||||
|
||||
''' Brute Force '''
|
||||
versions.extend(brute_force(cpv, url, options, output))
|
||||
|
||||
if versions and options['oneshot']:
|
||||
break
|
||||
|
||||
newversions = {}
|
||||
|
||||
for url, version in versions:
|
||||
''' Try to keep the most specific urls (determinted by the length) '''
|
||||
if version in newversions and len(url) < len(newversions[version]):
|
||||
continue
|
||||
''' Remove blacklisted versions '''
|
||||
if versionBlacklisted(pkg.cp, version, output):
|
||||
continue
|
||||
|
||||
newversions[version] = url
|
||||
|
||||
print ()
|
||||
|
||||
for version in newversions:
|
||||
print ("Upstream Version:"
|
||||
+ pp.number("%s" % version)
|
||||
+ pp.path(" %s" % newversions[version]))
|
||||
|
||||
if not len(newversions):
|
||||
print (pp.warn("Didn't find any new version,"
|
||||
+ "check package's homepage for "
|
||||
+ "more informations"));
|
||||
return versions
|
||||
|
||||
|
||||
def main():
|
||||
"""Parse command line and execute all actions."""
|
||||
# set default options
|
||||
options = {}
|
||||
options['nocolor'] = (port_settings["NOCOLOR"] in ('yes','true')
|
||||
or not sys.stdout.isatty())
|
||||
if options['nocolor']:
|
||||
pp.output.nocolor()
|
||||
# parse command line options and actions
|
||||
try:
|
||||
package = parseArgs(options)
|
||||
# filter exception to know what message to display
|
||||
except ParseArgsException as e:
|
||||
if e.value == 'help':
|
||||
printUsage(help='all')
|
||||
sys.exit(0)
|
||||
elif e.value[:5] == 'help-':
|
||||
printUsage(help=e.value[5:])
|
||||
sys.exit(0)
|
||||
elif e.value == 'version':
|
||||
printVersion()
|
||||
sys.exit(0)
|
||||
else:
|
||||
printUsage(e.value)
|
||||
sys.exit(errno.EINVAL)
|
||||
|
||||
output = EOutput(options['quiet'])
|
||||
scanUpstream(options, package, output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
setupSignals()
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print( "Aborted.")
|
||||
sys.exit(errno.EINTR)
|
||||
sys.exit(0)
|
@ -120,12 +120,11 @@ class Command(BaseCommand):
|
||||
|
||||
herd, created = Herd.objects.get_or_create(herd=name)
|
||||
|
||||
if created or herd.email != email:
|
||||
if not options['quiet']:
|
||||
sys.stdout.write('+ [h] %s <%s>\n' % (name, email))
|
||||
if created and not options['quiet']:
|
||||
sys.stdout.write('+ [h] %s <%s>\n' % (name, email))
|
||||
|
||||
herd.email = email
|
||||
herd.save()
|
||||
herd.email = email
|
||||
herd.save()
|
||||
|
||||
return herd
|
||||
|
||||
|
@ -154,11 +154,6 @@ class Command(BaseCommand):
|
||||
' Set all versions dead, then set found versions alive and delete old versions '
|
||||
Version.objects.filter(package=obj, packaged=True).update(alive=False)
|
||||
|
||||
obj.n_packaged = 0
|
||||
obj.n_overlay = 0
|
||||
obj.n_versions = Version.objects.filter(package=obj).count()
|
||||
obj.save()
|
||||
|
||||
return obj
|
||||
|
||||
def store_version(self, options, package, cpv, slot, overlay):
|
||||
|
@ -136,9 +136,6 @@ class Command(BaseCommand):
|
||||
' Set all versions dead, then set found versions alive and delete old versions '
|
||||
Version.objects.filter(package=obj, packaged=False).update(alive=False)
|
||||
|
||||
obj.n_versions = Version.objects.filter(package=obj).count()
|
||||
obj.save()
|
||||
|
||||
return obj
|
||||
|
||||
def store_version(self, options, package, ver, url):
|
||||
|
@ -17,14 +17,14 @@
|
||||
# eix-update
|
||||
|
||||
## Scan portage (packages, versions)
|
||||
# python manage.py scan-portage --all --purge
|
||||
# python manage.py scan-portage --all --purge-versions --purge-packages
|
||||
|
||||
## Scan metadata (herds, maintainers, homepages, ...)
|
||||
# python manage.py scan-metadata --all
|
||||
|
||||
## Scan uptsream packages
|
||||
# python manage.py scan-upstream --all
|
||||
# eix --only-names -x | gparallel --jobs 400% euscan | python manage.py scan-upstream --feed
|
||||
# eix --only-names -x | gparallel --jobs 400% euscan | python manage.py scan-upstream --feed --purge-versions
|
||||
|
||||
## Update counters
|
||||
# python manage.py update-counters
|
49
pym/euscan/__init__.py
Normal file
49
pym/euscan/__init__.py
Normal file
@ -0,0 +1,49 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright 2011 Corentin Chary <corentin.chary@gmail.com>
|
||||
# Distributed under the terms of the GNU General Public License v2
|
||||
|
||||
import sys
|
||||
|
||||
from portage.output import EOutput
|
||||
|
||||
CONFIG = {
|
||||
'nocolor': False,
|
||||
'quiet': False,
|
||||
'verbose': True,
|
||||
'debug': False,
|
||||
'brute-force': 3,
|
||||
'brute-force-recursive': True,
|
||||
'scan-dir': True,
|
||||
'oneshot': False,
|
||||
'user-agent' : 'Mozilla/5.0 (compatible; euscan; +http://euscan.iksaif.net)'
|
||||
}
|
||||
|
||||
output = EOutput(CONFIG['quiet'])
|
||||
|
||||
BLACKLIST_VERSIONS = [
|
||||
# Compatibility package for running binaries linked against a pre gcc 3.4 libstdc++, won't be updated
|
||||
'>=sys-libs/libstdc++-v3-3.4',
|
||||
]
|
||||
|
||||
BLACKLIST_PACKAGES = [
|
||||
# These kernels are almost dead
|
||||
'sys-kernel/usermode-sources',
|
||||
'sys-kernel/xbox-sources',
|
||||
'sys-kernel/cell-sources',
|
||||
]
|
||||
|
||||
SCANDIR_BLACKLIST_URLS = [
|
||||
'mirror://rubygems/(.*)', # Not browsable
|
||||
'mirror://gentoo/(.*)' # Directory too big
|
||||
]
|
||||
|
||||
BRUTEFORCE_BLACKLIST_PACKAGES = [
|
||||
'net-zope/plonepopoll' # infinite loop any http://plone.org/products/plonepopoll/releases/*/plonepopoll-2-6-1.tgz link will work
|
||||
]
|
||||
|
||||
BRUTEFORCE_BLACKLIST_URLS = [
|
||||
'http://(.*)dockapps.org/download.php/id/(.*)', # infinite loop
|
||||
'http://hydra.nixos.org/build/(.*)', # infinite loop
|
||||
'http://www.rennings.net/gentoo/distfiles/(.*)' # Doesn't respect 404, infinite loop
|
||||
]
|
24
pym/euscan/handlers/__init__.py
Normal file
24
pym/euscan/handlers/__init__.py
Normal file
@ -0,0 +1,24 @@
|
||||
from euscan.handlers import generic
|
||||
from euscan.handlers import php
|
||||
from euscan.handlers import pypi
|
||||
from euscan.handlers import rubygem
|
||||
|
||||
handlers = [ php, pypi, rubygem, generic ]
|
||||
|
||||
def find_best_handler(cpv, url):
|
||||
for handler in handlers:
|
||||
if handler.can_handle(cpv, url):
|
||||
return handler
|
||||
return None
|
||||
|
||||
def scan(cpv, url):
|
||||
handler = find_best_handler(cpv, url)
|
||||
if handler:
|
||||
return handler.scan(cpv, url)
|
||||
return []
|
||||
|
||||
def brute_force(cpv, url):
|
||||
handler = find_best_handler(cpv, url)
|
||||
if handler:
|
||||
return handler.brute_force(cpv, url)
|
||||
return []
|
183
pym/euscan/handlers/generic.py
Normal file
183
pym/euscan/handlers/generic.py
Normal file
@ -0,0 +1,183 @@
|
||||
import urllib2
|
||||
import re
|
||||
import StringIO
|
||||
|
||||
from BeautifulSoup import BeautifulSoup
|
||||
|
||||
import portage
|
||||
|
||||
from euscan import CONFIG, SCANDIR_BLACKLIST_URLS, BRUTEFORCE_BLACKLIST_PACKAGES, BRUTEFORCE_BLACKLIST_URLS, output
|
||||
from euscan import helpers
|
||||
|
||||
def scan_html(data, url, pattern):
|
||||
soup = BeautifulSoup(data)
|
||||
results = []
|
||||
|
||||
for link in soup.findAll('a'):
|
||||
href = link.get("href")
|
||||
if not href:
|
||||
continue
|
||||
if href.startswith(url):
|
||||
href = href.replace(url, "", 1)
|
||||
|
||||
match = re.match(pattern, href, re.I)
|
||||
if match:
|
||||
results.append((match.group(1), match.group(0)))
|
||||
|
||||
return results
|
||||
|
||||
def scan_ftp(data, url, pattern):
|
||||
buf = StringIO.StringIO(data)
|
||||
results = []
|
||||
|
||||
for line in buf.readlines():
|
||||
line = line.replace("\n", "").replace("\r", "")
|
||||
match = re.search(pattern, line, re.I)
|
||||
if match:
|
||||
results.append((match.group(1), match.group(0)))
|
||||
|
||||
return results
|
||||
|
||||
def scan_directory_recursive(cpv, url, steps):
|
||||
if not steps:
|
||||
return []
|
||||
|
||||
cp, ver, rev = portage.pkgsplit(cpv)
|
||||
url += steps[0][0]
|
||||
pattern = steps[0][1]
|
||||
|
||||
steps = steps[1:]
|
||||
|
||||
output.einfo("Scanning: %s" % url)
|
||||
|
||||
try:
|
||||
fp = helpers.urlopen(url)
|
||||
except urllib2.URLError:
|
||||
return []
|
||||
except IOError:
|
||||
return []
|
||||
|
||||
data = fp.read()
|
||||
|
||||
results = []
|
||||
|
||||
if re.search("<\s*a\s+[^>]*href", data):
|
||||
results.extend(scan_html(data, url, pattern))
|
||||
elif url.startswith('ftp://'):
|
||||
results.extend(scan_ftp(data, url, pattern))
|
||||
|
||||
versions = []
|
||||
|
||||
for version, path in results:
|
||||
if helpers.version_filtered(cp, ver, version):
|
||||
continue
|
||||
|
||||
if not url.endswith('/') and not path.startswith('/'):
|
||||
path = url + '/' + path
|
||||
else:
|
||||
path = url + path
|
||||
|
||||
versions.append((path, version))
|
||||
|
||||
if steps:
|
||||
ret = scan_directory_recursive(cpv, path, steps)
|
||||
versions.extend(ret)
|
||||
|
||||
return versions
|
||||
|
||||
def scan(cpv, url):
|
||||
for bu in SCANDIR_BLACKLIST_URLS:
|
||||
if re.match(bu, url):
|
||||
output.einfo("%s is blacklisted by rule %s" % (url, bu))
|
||||
return []
|
||||
|
||||
resolved_url = helpers.parse_mirror(url)
|
||||
|
||||
cp, ver, rev = portage.pkgsplit(cpv)
|
||||
|
||||
template = helpers.template_from_url(resolved_url, ver)
|
||||
if '${' not in template:
|
||||
output.einfo("Url doesn't seems to depend on version: %s not found in %s"
|
||||
% (ver, resolved_url))
|
||||
return []
|
||||
else:
|
||||
output.einfo("Scanning: %s" % template)
|
||||
|
||||
steps = helpers.generate_scan_paths(template)
|
||||
return scan_directory_recursive(cpv, "", steps)
|
||||
|
||||
def brute_force(cpv, url):
|
||||
cp, ver, rev = portage.pkgsplit(cpv)
|
||||
|
||||
url = helpers.parse_mirror(url)
|
||||
|
||||
for bp in BRUTEFORCE_BLACKLIST_PACKAGES:
|
||||
if re.match(bp, cp):
|
||||
output.einfo("%s is blacklisted by rule %s" % (cp, bp))
|
||||
return []
|
||||
|
||||
for bp in BRUTEFORCE_BLACKLIST_URLS:
|
||||
if re.match(bp, url):
|
||||
output.einfo("%s is blacklisted by rule %s" % (cp, bp))
|
||||
return []
|
||||
|
||||
output.einfo("Generating version from " + ver)
|
||||
|
||||
components = helpers.split_version(ver)
|
||||
versions = helpers.gen_versions(components, CONFIG["brute-force"])
|
||||
|
||||
""" Remove unwanted versions """
|
||||
for v in versions:
|
||||
if helpers.vercmp(cp, ver, helpers.join_version(v)) >= 0:
|
||||
versions.remove(v)
|
||||
|
||||
if not versions:
|
||||
output.einfo("Can't generate new versions from " + ver)
|
||||
return []
|
||||
|
||||
template = helpers.template_from_url(url, ver)
|
||||
|
||||
if '${PV}' not in template:
|
||||
output.einfo("Url doesn't seems to depend on full version: %s not found in %s"
|
||||
% (ver, url))
|
||||
return []
|
||||
else:
|
||||
output.einfo("Brute forcing: %s" % template)
|
||||
|
||||
result = []
|
||||
|
||||
i = 0
|
||||
done = []
|
||||
|
||||
while i < len(versions):
|
||||
components = versions[i]
|
||||
i += 1
|
||||
if components in done:
|
||||
continue
|
||||
done.append(tuple(components))
|
||||
|
||||
version = helpers.join_version(components)
|
||||
|
||||
if helpers.version_filtered(cp, ver, version):
|
||||
continue
|
||||
|
||||
url = helpers.url_from_template(template, version)
|
||||
infos = helpers.tryurl(url, template)
|
||||
|
||||
if not infos:
|
||||
continue
|
||||
|
||||
result.append([url, version])
|
||||
|
||||
if CONFIG["brute-force-recursive"]:
|
||||
for v in helpers.gen_versions(components, CONFIG["brute-force"]):
|
||||
if v not in versions and tuple(v) not in done:
|
||||
versions.append(v)
|
||||
|
||||
if CONFIG["oneshot"]:
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
def can_handle(cpv, url):
|
||||
return True
|
65
pym/euscan/handlers/php.py
Normal file
65
pym/euscan/handlers/php.py
Normal file
@ -0,0 +1,65 @@
|
||||
import re
|
||||
import portage
|
||||
import urllib2
|
||||
import xml.dom.minidom
|
||||
|
||||
from euscan import helpers, output
|
||||
|
||||
def can_handle(cpv, url):
|
||||
if url.startswith('http://pear.php.net/get/'):
|
||||
return True
|
||||
if url.startswith('http://pecl.php.net/get/'):
|
||||
return True
|
||||
return False
|
||||
|
||||
def guess_package_and_channel(cp, url):
|
||||
match = re.search('http://(.*)/get/(.*)-(.*).tgz', url)
|
||||
|
||||
if match:
|
||||
host = match.group(1)
|
||||
pkg = match.group(2)
|
||||
else:
|
||||
cat, pkg = cp.split("/")
|
||||
|
||||
return pkg, host
|
||||
|
||||
def scan(cpv, url):
|
||||
pkg, channel = guess_package_and_channel(cpv, url)
|
||||
|
||||
orig_url = url
|
||||
url = 'http://%s/rest/r/%s/allreleases.xml' % (channel, pkg.lower())
|
||||
|
||||
output.einfo("Using: " + url)
|
||||
|
||||
try:
|
||||
fp = helpers.urlopen(url)
|
||||
except urllib2.URLError:
|
||||
return []
|
||||
except IOError:
|
||||
return []
|
||||
|
||||
data = fp.read()
|
||||
|
||||
dom = xml.dom.minidom.parseString(data)
|
||||
|
||||
nodes = dom.getElementsByTagName("v")
|
||||
ret = []
|
||||
|
||||
cp, ver, rev = portage.pkgsplit(cpv)
|
||||
|
||||
for node in nodes:
|
||||
version = node.childNodes[0].data
|
||||
if helpers.version_filtered(cp, ver, version):
|
||||
continue
|
||||
|
||||
url = 'http://%s/get/%s-%s.tgz' % (channel, pkg, version)
|
||||
|
||||
if url == orig_url:
|
||||
continue
|
||||
|
||||
ret.append(( url, version ))
|
||||
|
||||
return ret
|
||||
|
||||
def brute_force(cpv, url):
|
||||
return []
|
51
pym/euscan/handlers/pypi.py
Normal file
51
pym/euscan/handlers/pypi.py
Normal file
@ -0,0 +1,51 @@
|
||||
import xmlrpclib
|
||||
import pprint
|
||||
import re
|
||||
|
||||
import portage
|
||||
|
||||
from euscan import helpers, output
|
||||
|
||||
def can_handle(cpv, url):
|
||||
return url.startswith('mirror://pypi/')
|
||||
|
||||
def guess_package(cp, url):
|
||||
match = re.search('mirror://pypi/\w+/(.*)/.*', url)
|
||||
if match:
|
||||
return match.group(1)
|
||||
|
||||
cat, pkg = cp.split("/")
|
||||
|
||||
return pkg
|
||||
|
||||
def scan(cpv, url):
|
||||
'http://wiki.python.org/moin/PyPiXmlRpc'
|
||||
|
||||
|
||||
package = guess_package(cpv, url)
|
||||
|
||||
output.einfo("Using PyPi XMLRPC: " + package)
|
||||
|
||||
client = xmlrpclib.ServerProxy('http://pypi.python.org/pypi')
|
||||
versions = client.package_releases(package)
|
||||
|
||||
if not versions:
|
||||
return versions
|
||||
|
||||
versions.reverse()
|
||||
|
||||
cp, ver, rev = portage.pkgsplit(cpv)
|
||||
|
||||
ret = []
|
||||
|
||||
for version in versions:
|
||||
if helpers.version_filtered(cp, ver, version):
|
||||
continue
|
||||
urls = client.release_urls(package, version)
|
||||
urls = " ".join([ infos['url'] for infos in urls ])
|
||||
ret.append(( urls, version ))
|
||||
|
||||
return ret
|
||||
|
||||
def brute_force(cpv, url):
|
||||
return []
|
56
pym/euscan/handlers/rubygem.py
Normal file
56
pym/euscan/handlers/rubygem.py
Normal file
@ -0,0 +1,56 @@
|
||||
import re
|
||||
import portage
|
||||
import json
|
||||
import urllib2
|
||||
|
||||
from euscan import helpers, output
|
||||
|
||||
def can_handle(cpv, url):
|
||||
return url.startswith('mirror://rubygems/')
|
||||
|
||||
def guess_gem(cpv, url):
|
||||
match = re.search('mirror://rubygems/(.*).gem', url)
|
||||
if match:
|
||||
cpv = 'fake/%s' % match.group(1)
|
||||
|
||||
cp, ver, rev = portage.pkgsplit(cpv)
|
||||
cat, pkg = cp.split("/")
|
||||
|
||||
return pkg
|
||||
|
||||
def scan(cpv, url):
|
||||
'http://guides.rubygems.org/rubygems-org-api/#gemversion'
|
||||
|
||||
gem = guess_gem(cpv, url)
|
||||
url = 'http://rubygems.org/api/v1/versions/%s.json' % gem
|
||||
|
||||
output.einfo("Using: " + url)
|
||||
|
||||
try:
|
||||
fp = helpers.urlopen(url, None, 5)
|
||||
except urllib2.URLError:
|
||||
return []
|
||||
except IOError:
|
||||
return []
|
||||
|
||||
data = fp.read()
|
||||
versions = json.loads(data)
|
||||
|
||||
if not versions:
|
||||
return []
|
||||
|
||||
cp, ver, rev = portage.pkgsplit(cpv)
|
||||
|
||||
ret = []
|
||||
|
||||
for version in versions:
|
||||
version = version['number']
|
||||
if helpers.version_filtered(cp, ver, version):
|
||||
continue
|
||||
url = 'http://rubygems.org/gems/%s-%s.gem' % (gem, version)
|
||||
ret.append(( url, version ))
|
||||
|
||||
return ret
|
||||
|
||||
def brute_force(cpv, url):
|
||||
return []
|
309
pym/euscan/helpers.py
Normal file
309
pym/euscan/helpers.py
Normal file
@ -0,0 +1,309 @@
|
||||
import urllib2
|
||||
import os
|
||||
import re
|
||||
import pkg_resources
|
||||
import errno
|
||||
|
||||
import portage
|
||||
from portage import dep
|
||||
|
||||
from euscan import CONFIG, BLACKLIST_VERSIONS, output
|
||||
|
||||
def htop_vercmp(a, b):
|
||||
def fixver(v):
|
||||
if v in ['0.11', '0.12', '0.13']:
|
||||
v = '0.1.' + v[3:]
|
||||
return v
|
||||
|
||||
return simple_vercmp(fixver(a), fixver(b))
|
||||
|
||||
VERSION_CMP_PACKAGE_QUIRKS = {
|
||||
'sys-process/htop' : htop_vercmp
|
||||
}
|
||||
|
||||
_v = r'((\d+)((\.\d+)*)([a-zA-Z]*?)(((-|_)(pre|p|beta|b|alpha|a|rc|r)\d*)*))'
|
||||
|
||||
def cast_int_components(version):
|
||||
for i, obj in enumerate(version):
|
||||
try:
|
||||
version[i] = int(obj)
|
||||
except ValueError:
|
||||
pass
|
||||
return version
|
||||
|
||||
def simple_vercmp(a, b):
|
||||
if a == b:
|
||||
return 0
|
||||
|
||||
# For sane versions
|
||||
r = portage.versions.vercmp(a, b)
|
||||
|
||||
if r is not None:
|
||||
return r
|
||||
|
||||
# Fallback
|
||||
a = pkg_resources.parse_version(a)
|
||||
b = pkg_resources.parse_version(b)
|
||||
|
||||
if a < b:
|
||||
return -1
|
||||
else:
|
||||
return 1
|
||||
|
||||
def vercmp(package, a, b):
|
||||
if package in VERSION_CMP_PACKAGE_QUIRKS:
|
||||
return VERSION_CMP_PACKAGE_QUIRKS[package](a, b)
|
||||
return simple_vercmp(a, b)
|
||||
|
||||
def version_is_nightly(a, b):
|
||||
a = pkg_resources.parse_version(a)
|
||||
b = pkg_resources.parse_version(b)
|
||||
|
||||
''' Try to skip nightly builds when not wanted (www-apps/moodle) '''
|
||||
if len(a) != len(b) and len(b) == 2 and len(b[0]) == len('yyyymmdd'):
|
||||
return True
|
||||
return False
|
||||
|
||||
def version_blacklisted(cp, version):
|
||||
rule = None
|
||||
cpv = '%s-%s' % (cp, version)
|
||||
|
||||
''' Check that the generated cpv can be used by portage '''
|
||||
if not portage.versions.catpkgsplit(cpv):
|
||||
return False
|
||||
|
||||
for bv in BLACKLIST_VERSIONS:
|
||||
if dep.match_from_list(bv, [cpv]):
|
||||
rule = bv
|
||||
None
|
||||
|
||||
if rule:
|
||||
output.einfo("%s is blacklisted by rule %s" % (cpv, bv))
|
||||
return rule is not None
|
||||
|
||||
def version_filtered(cp, base, version):
|
||||
if vercmp(cp, base, version) >= 0:
|
||||
return True
|
||||
|
||||
if version_blacklisted(cp, version):
|
||||
return True
|
||||
|
||||
if version_is_nightly(base, version):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def generate_templates_vars(version):
|
||||
ret = []
|
||||
|
||||
part = split_version(version)
|
||||
for i in range(2, len(part)):
|
||||
ver = []
|
||||
var = []
|
||||
for j in range(i):
|
||||
ver.append(str(part[j]))
|
||||
var.append('${%d}' % j)
|
||||
|
||||
ret.append((".".join(ver), ".".join(var)))
|
||||
ret.append((version, '${PV}'))
|
||||
ret.reverse()
|
||||
return ret
|
||||
|
||||
def template_from_url(url, version):
|
||||
prefix, chunks = url.split('://')
|
||||
chunks = chunks.split('/')
|
||||
|
||||
for i in range(len(chunks)):
|
||||
chunk = chunks[i]
|
||||
|
||||
subs = generate_templates_vars(version)
|
||||
for sub in subs:
|
||||
chunk = chunk.replace(sub[0], sub[1])
|
||||
|
||||
chunks[i] = chunk
|
||||
|
||||
|
||||
return prefix + "://" + "/".join(chunks)
|
||||
|
||||
def url_from_template(url, version):
|
||||
components = split_version(version)
|
||||
|
||||
url = url.replace('${PV}', version)
|
||||
for i in range(len(components)):
|
||||
url = url.replace('${%d}' % i, str(components[i]))
|
||||
|
||||
return url
|
||||
|
||||
# Stolen from distutils.LooseVersion
|
||||
# Used for brute force to increment the version
|
||||
def split_version(version):
|
||||
component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE)
|
||||
components = filter(lambda x: x and x != '.', component_re.split(version))
|
||||
for i in range(len(components)):
|
||||
try:
|
||||
components[i] = int(components[i])
|
||||
except ValueError:
|
||||
pass
|
||||
return components
|
||||
|
||||
def join_version(components):
|
||||
version = ""
|
||||
for i in range(len(components)):
|
||||
version += str(components[i])
|
||||
if i >= len(components) - 1:
|
||||
break
|
||||
if type(components[i]) != str and type(components[i + 1]) != str:
|
||||
version += "."
|
||||
return version
|
||||
|
||||
def increment_version(components, level):
|
||||
n = len(components)
|
||||
|
||||
if level > n - 1 or level < 0:
|
||||
raise Exception
|
||||
|
||||
for i in range(n, level + 1, -1):
|
||||
if type(components[i - 1]) == int:
|
||||
components[i - 1] = 0
|
||||
|
||||
if type(components[level]) == int:
|
||||
components[level] += 1
|
||||
|
||||
return components
|
||||
|
||||
def gen_versions(components, level):
|
||||
n = len(components)
|
||||
depth = level
|
||||
level = min(level, n)
|
||||
|
||||
if not n:
|
||||
return []
|
||||
|
||||
versions = []
|
||||
|
||||
for i in range(n, n - level, -1):
|
||||
increment_version(components, i - 1)
|
||||
for j in range(depth):
|
||||
versions.append(list(components))
|
||||
increment_version(components, i - 1)
|
||||
|
||||
return versions
|
||||
|
||||
def urlopen(url, timeout=None):
|
||||
|
||||
if not timeout:
|
||||
if 'sourceforge' in url:
|
||||
timeout = 15
|
||||
else:
|
||||
timeout = 5
|
||||
|
||||
request = urllib2.Request(url)
|
||||
request.add_header('User-Agent', CONFIG['user-agent'])
|
||||
return urllib2.urlopen(request, None, timeout)
|
||||
|
||||
def tryurl(fileurl, template):
|
||||
result = True
|
||||
|
||||
output.ebegin("Trying: " + fileurl)
|
||||
|
||||
try:
|
||||
basename = os.path.basename(fileurl)
|
||||
|
||||
fp = urlopen(fileurl)
|
||||
headers = fp.info()
|
||||
|
||||
if 'Content-disposition' in headers and basename not in headers['Content-disposition']:
|
||||
result = None
|
||||
elif 'Content-Length' in headers and headers['Content-Length'] == '0':
|
||||
result = None
|
||||
elif 'text/html' in headers['Content-Type']:
|
||||
result = None
|
||||
elif fp.geturl() != fileurl:
|
||||
regex = regex_from_template(template)
|
||||
baseregex = regex_from_template(os.path.basename(template))
|
||||
basename2 = os.path.basename(fp.geturl())
|
||||
|
||||
# Redirect to another (earlier?) version
|
||||
if basename != basename2 and (re.match(regex, fp.geturl()) or re.match(baseregex, basename2)):
|
||||
result = None
|
||||
|
||||
|
||||
if result:
|
||||
result = (fp.geturl(), fp.info())
|
||||
|
||||
except urllib2.URLError:
|
||||
result = None
|
||||
except IOError:
|
||||
result = None
|
||||
|
||||
output.eend(errno.ENOENT if not result else 0)
|
||||
|
||||
return result
|
||||
|
||||
def regex_from_template(template):
|
||||
template = re.escape(template)
|
||||
template = template.replace('\$\{', '${')
|
||||
template = template.replace('\}', '}')
|
||||
template = template.replace('}\.$', '}.$')
|
||||
template = template.replace('${1}', r'([\d]+?)')
|
||||
template = re.sub(r'(\$\{\d+\}\.?)+', r'([\w]+?)', template)
|
||||
#template = re.sub(r'(\$\{\d+\}\.?)+', r'([\w]+?)', template)
|
||||
#template = re.sub(r'(\$\{\d+\}\.+)+', '(.+?)\.', template)
|
||||
#template = re.sub(r'(\$\{\d+\})+', '(.+?)', template)
|
||||
template = template.replace('${PV}', _v)
|
||||
template = template + r'/?$'
|
||||
return template
|
||||
|
||||
def basedir_from_template(template):
|
||||
idx = template.find('${')
|
||||
if idx == -1:
|
||||
return template
|
||||
|
||||
idx = template[0:idx].rfind('/')
|
||||
if idx == -1:
|
||||
return ""
|
||||
|
||||
return template[0:idx]
|
||||
|
||||
def generate_scan_paths(url):
|
||||
prefix, chunks = url.split('://')
|
||||
chunks = chunks.split('/')
|
||||
|
||||
steps = []
|
||||
|
||||
path = prefix + ":/"
|
||||
for chunk in chunks:
|
||||
if '${' in chunk:
|
||||
steps.append((path, regex_from_template(chunk)))
|
||||
path = ""
|
||||
else:
|
||||
path += "/"
|
||||
path += chunk
|
||||
return steps
|
||||
|
||||
def parse_mirror(uri):
|
||||
from random import shuffle
|
||||
|
||||
mirrors = portage.settings.thirdpartymirrors()
|
||||
|
||||
if not uri.startswith("mirror://"):
|
||||
return uri
|
||||
|
||||
eidx = uri.find("/", 9)
|
||||
if eidx == -1:
|
||||
output.einfo("Invalid mirror definition in SRC_URI:\n")
|
||||
output.einfo(" %s\n" % (uri))
|
||||
return None
|
||||
|
||||
mirrorname = uri[9:eidx]
|
||||
path = uri[eidx+1:]
|
||||
|
||||
if mirrorname in mirrors:
|
||||
mirrors = mirrors[mirrorname]
|
||||
shuffle(mirrors)
|
||||
uri = mirrors[0].strip("/") + "/" + path
|
||||
else:
|
||||
output.einfo("No known mirror by the name: %s\n" % (mirrorname))
|
||||
return None
|
||||
|
||||
return uri
|
130
pym/euscan/scan.py
Normal file
130
pym/euscan/scan.py
Normal file
@ -0,0 +1,130 @@
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import time
|
||||
import getopt
|
||||
import random
|
||||
import urllib2
|
||||
import StringIO
|
||||
|
||||
import pkg_resources
|
||||
|
||||
import portage
|
||||
import portage.versions
|
||||
from portage import dep
|
||||
from portage.dbapi import porttree
|
||||
from portage.output import white, yellow, turquoise, green, teal, red, EOutput
|
||||
|
||||
import gentoolkit.pprinter as pp
|
||||
from gentoolkit import errors
|
||||
from gentoolkit.query import Query
|
||||
from gentoolkit.eclean.search import (port_settings)
|
||||
|
||||
from euscan import CONFIG, BLACKLIST_PACKAGES, output
|
||||
from euscan import handlers
|
||||
from euscan import helpers
|
||||
|
||||
def filter_versions(cp, versions):
|
||||
filtered = {}
|
||||
|
||||
for url, version in versions:
|
||||
|
||||
''' Try to keep the most specific urls (determinted by the length) '''
|
||||
if version in filtered and len(url) < len(filtered[version]):
|
||||
continue
|
||||
|
||||
''' Remove blacklisted versions '''
|
||||
if helpers.version_blacklisted(cp, version):
|
||||
continue
|
||||
|
||||
filtered[version] = url
|
||||
|
||||
return [ (filtered[version], version) for version in filtered ]
|
||||
|
||||
def scan_upstream_urls(cpv, urls):
|
||||
versions = []
|
||||
|
||||
for filename in urls:
|
||||
for url in urls[filename]:
|
||||
print ()
|
||||
output.einfo("SRC_URI is '%s'" % url)
|
||||
|
||||
if '://' not in url:
|
||||
output.einfo("Invalid url '%s'" % url)
|
||||
continue
|
||||
|
||||
''' Try normal scan '''
|
||||
if CONFIG["scan-dir"]:
|
||||
versions.extend(handlers.scan(cpv, url))
|
||||
|
||||
if versions and CONFIG['oneshot']:
|
||||
break
|
||||
|
||||
''' Brute Force '''
|
||||
if CONFIG["brute-force"] > 0:
|
||||
versions.extend(handlers.brute_force(cpv, url))
|
||||
|
||||
if versions and CONFIG['oneshot']:
|
||||
break
|
||||
|
||||
cp, ver, rev = portage.pkgsplit(cpv)
|
||||
return filter_versions(cp, versions)
|
||||
|
||||
|
||||
def scan_upstream(query):
|
||||
matches = Query(query).find(
|
||||
include_masked=True,
|
||||
in_installed=False
|
||||
)
|
||||
|
||||
if not matches:
|
||||
sys.stderr.write(pp.warn("No package matching '%s'" % pp.pkgquery(query)))
|
||||
return []
|
||||
|
||||
matches = sorted(matches)
|
||||
pkg = matches.pop()
|
||||
|
||||
if '9999' in pkg.version:
|
||||
if len(matches) == 0:
|
||||
sys.stderr.write(pp.warn("Package '%s' only have a dev version (9999)" % pp.pkgquery(pkg.cp)))
|
||||
return []
|
||||
else:
|
||||
pkg = matches.pop()
|
||||
|
||||
if pkg.cp in BLACKLIST_PACKAGES:
|
||||
sys.stderr.write(pp.warn("Package '%s' is blacklisted" % pp.pkgquery(pkg.cp)))
|
||||
return []
|
||||
|
||||
pp.uprint(" * %s [%s]" % (pp.cpv(pkg.cpv), pp.section(pkg.repo_name())))
|
||||
pp.uprint()
|
||||
|
||||
ebuild_path = pkg.ebuild_path()
|
||||
if ebuild_path:
|
||||
pp.uprint('Ebuild: ' + pp.path(os.path.normpath(ebuild_path)))
|
||||
|
||||
pp.uprint('Repository: ' + pkg.repo_name())
|
||||
pp.uprint('Homepage: ' + pkg.environment("HOMEPAGE"))
|
||||
pp.uprint('Description: ' + pkg.environment("DESCRIPTION"))
|
||||
|
||||
cpv = pkg.cpv
|
||||
metadata = {
|
||||
"EAPI" : port_settings["EAPI"],
|
||||
"SRC_URI" : pkg.environment("SRC_URI", False),
|
||||
}
|
||||
use = frozenset(port_settings["PORTAGE_USE"].split())
|
||||
try:
|
||||
alist = porttree._parse_uri_map(cpv, metadata, use=use)
|
||||
aalist = porttree._parse_uri_map(cpv, metadata)
|
||||
except InvalidDependString as e:
|
||||
sys.stderr.write(pp.warn("%s\n" % str(e)))
|
||||
sys.stderr.write(pp.warn("Invalid SRC_URI for '%s'" % pp.pkgquery(cpv)))
|
||||
return []
|
||||
|
||||
if "mirror" in portage.settings.features:
|
||||
urls = aalist
|
||||
else:
|
||||
urls = alist
|
||||
|
||||
return scan_upstream_urls(pkg.cpv, urls)
|
54
setup.py
Executable file
54
setup.py
Executable file
@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import re
|
||||
import sys
|
||||
import distutils
|
||||
from distutils import core, log
|
||||
from glob import glob
|
||||
|
||||
import os
|
||||
import io
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'pym'))
|
||||
|
||||
__version__ = os.getenv('VERSION', default='9999')
|
||||
|
||||
cwd = os.getcwd()
|
||||
|
||||
# Load EPREFIX from Portage, fall back to the empty string if it fails
|
||||
try:
|
||||
from portage.const import EPREFIX
|
||||
except ImportError:
|
||||
EPREFIX='/'
|
||||
|
||||
# Python files that need `__version__ = ""` subbed, relative to this dir:
|
||||
python_scripts = [os.path.join(cwd, path) for path in (
|
||||
'bin/euscan',
|
||||
)]
|
||||
|
||||
packages = [
|
||||
str('.'.join(root.split(os.sep)[1:]))
|
||||
for root, dirs, files in os.walk('pym/euscan')
|
||||
if '__init__.py' in files
|
||||
]
|
||||
|
||||
core.setup(
|
||||
name='euscan',
|
||||
version=__version__,
|
||||
description='Ebuild Upstream Scan tools.',
|
||||
author='Corentin Chary',
|
||||
author_email='corentin.chary@gmail.com',
|
||||
maintainer='Corentin Chary',
|
||||
maintainer_email='corentin.chary@gmail.com',
|
||||
url='http://euscan.iksaif.net',
|
||||
download_url='http://git.iksaif.net/?p=euscan.git;a=snapshot;h=HEAD;sf=tgz',
|
||||
package_dir={'': 'pym'},
|
||||
packages=packages,
|
||||
package_data = {},
|
||||
scripts=python_scripts,
|
||||
data_files=(
|
||||
(os.path.join(EPREFIX, 'usr/share/man/man1'), glob('man/*')),
|
||||
),
|
||||
)
|
Loading…
Reference in New Issue
Block a user