Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
executable file 792 lines (725 sloc) 32.7 KB
#!/usr/bin/python3
"""
apt_changelog.py
Replacement for apt changelog, can be used both standalone and as a module.
In addition to apt changelog sources, it directly supports also Linux Mint
and Launchpad repositories and will try to retrieve a changelog for packages
from all other debian package sources as well. Most importantly, changelogs
for installed packages are retrieved locally to avoid unnecessary network
activity.
Unlike apt it does not support wildcards for the package name. You get to
check one specific changelog at a time only. I see no value in multi-lookups.
Copyright (c) 2018 gm10
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import fnmatch
import gzip
import lzma
import os
import sys
import tarfile
import tempfile
import requests
from apt.cache import Cache
from apt.debfile import DebPackage
import apt_pkg
class _Package():
def __init__(self,name:str, fullname:str, architecture:str, version:str,
source_name:str, source_version:str, uri:str, filename:str, origin:str,
component:str, downloadable:bool, is_installed:bool, dependencies:list):
self.name = name
self.fullname = fullname
self.architecture = architecture
self.version = version
self.source_name = source_name
self.source_version = source_version
self.source_version_raw = None
self.uri = uri
self.filename = filename
self.origin = origin
self.component = component
self.downloadable = downloadable
self.is_installed = is_installed
self.installed_files = list()
self.dependencies = dependencies
class _AptChangelog():
def __init__(self, interactive:bool=False):
self.interactive = interactive
# constants
# apt uses MB rather than MiB, so let's stay consistent
self.MB = 1000 ** 2
# downloads larger than this require confirmation or fail
self.max_download_size_default = 1.5 * self.MB
self.max_download_size = self.max_download_size_default
max_download_size_msg_template = "\
To retrieve the full changelog, %s MB have to be downloaded.\n%s\
\n\
Proceed with the download?"
self.max_download_size_msg_lc = max_download_size_msg_template % ("%.1f",
"Otherwise we will try to retrieve just the last change.\n")
self.max_download_size_msg = max_download_size_msg_template % ("%.1f","")
self.max_download_size_msg_unknown = max_download_size_msg_template % ("an unknown amount of", "")
self.apt_cache = None
self.apt_cache_date = None
self.candidate = None
# get apt's configuration
apt_pkg.init_config()
if apt_pkg.config.exists("Acquire::Changelogs::URI::Origin"):
self.apt_origins = apt_pkg.config.subtree("Acquire::Changelogs::URI::Origin")
else:
self.apt_origins = None
if apt_pkg.config.exists("Dir::Cache::pkgcache"):
self.apt_cache_path = apt_pkg.config.find_dir("Dir::Cache")
self.pkgcache = apt_pkg.config.find_file("Dir::Cache::pkgcache")
else:
self.apt_cache = "invalid"
if (self.apt_cache or
not os.path.isdir(self.apt_cache_path) or
not os.path.isfile(self.pkgcache)
):
print("E: Invalid APT configuration found, try to run `apt update` first",
file=sys.stderr)
self.close(99)
def get_cache_date(self):
if os.path.isfile(self.pkgcache):
return os.path.getmtime(self.pkgcache)
return None
def refresh_cache(self):
cache_date = self.get_cache_date()
if not self.apt_cache:
self.apt_cache = Cache()
self.apt_cache_date = cache_date
elif cache_date != self.apt_cache_date:
self.apt_cache.open(None)
self.apt_cache_date = cache_date
def drop_cache(self):
if self.candidate:
self.candidate = None
self.apt_cache = None
def get_changelog(self, pkg_name:str, no_local:bool=False):
self.refresh_cache()
self.candidate = self.parse_package_metadata(pkg_name)
# parse the package's origin
if not self.candidate.downloadable:
origin = "local_package"
elif self.candidate.origin == "linuxmint":
origin = "linuxmint"
elif self.candidate.origin.startswith("LP-PPA-"):
origin = "LP-PPA"
elif self.apt_origins and self.candidate.origin in self.apt_origins.list():
origin = "APT"
else:
origin = "unsupported"
# Check for changelog of installed package first
has_local_changelog = False
uri = None
if not no_local and self.candidate.is_installed:
if _DEBUG: print("Package is installed...")
uri = self.get_changelog_from_filelist(
self.candidate.installed_files, local=True)
# Ubuntu kernel workarounds
if self.candidate.origin == "Ubuntu":
if self.candidate.source_name == "linux-signed":
uri = uri.replace("linux-image","linux-modules")
if self.candidate.source_name == "linux-meta":
uri = None
if uri and not os.path.isfile(uri):
uri = None
# Do nothing if local changelog exists
if uri:
has_local_changelog = True
# all origins that APT supports
elif origin == 'APT':
uri = self.get_apt_changelog_uri(
self.apt_origins.get(self.candidate.origin))
r = self.check_url(uri)
if not r:
self.exit_on_fail(2)
# Linux Mint repo
elif origin == 'linuxmint':
# Mint repos don't have .debian.tar.xz files, only full packages, so
# check the package cache first
base_uri, _ = os.path.split(self.candidate.uri)
r, uri = self.get_changelog_uri(base_uri)
if not r:
# fall back to last change info for the source package
# Mint's naming scheme seems to be using amd64 unless source
# is i386 only, we always check amd64 first
base_uri = "http://packages.linuxmint.com/dev/%s_%s_%s.changes"
uri = base_uri % (self.candidate.source_name,
self.candidate.source_version, "amd64")
r = self.check_url(uri, False)
if not r:
uri = base_uri % (self.candidate.source_name,
self.candidate.source_version, "i386")
r = self.check_url(uri, False)
if not r:
self.exit_on_fail(3)
# Launchpad PPA
elif origin == 'LP-PPA':
ppa_owner, ppa_name, _ = \
self.candidate.uri.split("ppa.launchpad.net/")[1].split("/", 2)
base_uri = f"http://ppa.launchpad.net/{ppa_owner}/{ppa_name}/ubuntu/pool/main/{self.source_prefix()}/{self.candidate.source_name}"
r, uri = self.get_changelog_uri(base_uri)
if not r:
# fall back to last change info only
uri = f"https://launchpad.net/~{ppa_owner}/+archive/ubuntu/{ppa_name}/+files/{self.candidate.source_name}_{self.candidate.source_version}_source.changes"
r = self.check_url(uri, False)
if not r:
self.exit_on_fail(4)
# Not supported origin
elif origin == 'unsupported':
if _DEBUG: print("Unsupported Package")
base_uri, _ = os.path.split(self.candidate.uri)
r, uri = self.get_changelog_uri(base_uri)
if not r:
self.exit_on_fail(5)
# Locally installed package without local changelog or remote
# source, hope it's cached and contains a changelog
elif origin == 'local_package':
uri = self.apt_cache_path + self.candidate.filename
if not os.path.isfile(uri):
self.exit_on_fail(6)
# Changelog downloading, extracting and processing:
changelog = ""
# local changelog
if has_local_changelog and not no_local:
if _DEBUG: print("Using local changelog:",uri)
try:
filename = os.path.basename(uri)
# determine file type by name/extension
# as per debian policy 4.4 the encoding must be UTF-8
# as per policy 12.7 the name must be changelog.Debian.gz or
# changelog.gz (deprecated)
if filename.lower().endswith('.gz'):
changelog = gzip.open(uri,'r').read().decode('utf-8')
elif filename.lower().endswith('.xz'):
# just in case / future proofing
changelog = lzma.open(uri,'r').read().decode('utf-8')
elif filename.lower() == 'changelog':
changelog = open(uri, 'r').read().encode().decode('utf-8')
else:
raise ValueError('Unknown changelog format')
except Exception as e:
_generic_exception_handler(e)
self.exit_on_fail(1)
# APT-format changelog, download directly
# - unfortunately this is slow since the servers support no compression
elif origin == "APT":
if _DEBUG: print("Downloading: %s (%.2f MB)" % (uri, r.length / self.MB))
changelog = r.text
r.close()
# last change changelog, download directly
elif uri.endswith('.changes'):
if _DEBUG: print("Downloading: %s (%.2f MB)" % (uri, r.length / self.MB))
changes = r.text.split("Changes:")[1].split("Checksums")[0].split("\n")
r.close()
for change in changes:
change = change.strip()
if change:
if change == ".":
change = ""
changelog += change + "\n"
# compressed binary source, download and extract changelog
else:
source_is_cache = uri.startswith(self.apt_cache_path)
if _DEBUG: print("Using cached package:" if source_is_cache else
"Downloading: %s (%.2f MB)" % (uri, r.length / self.MB))
try:
if not source_is_cache:
# download stream to temporary file
tmpFile = tempfile.NamedTemporaryFile(prefix="apt-changelog-")
if self.interactive and r.length:
# download chunks with progress indicator
recv_length = 0
blocks = 60
for data in r.iter_content(chunk_size=16384):
recv_length += len(data)
tmpFile.write(data)
recv_pct = recv_length / r.length
recv_blocks = int(blocks * recv_pct)
print("\r[%(progress)s%(spacer)s] %(percentage).1f%%" %
{
"progress": "=" * recv_blocks,
"spacer": " " * (blocks - recv_blocks),
"percentage": recv_pct * 100
}, end="", flush=True)
# clear progress bar when done
print("\r" + " " * (blocks + 10), end="\r", flush=True)
else:
# no content-length or non-interactive, download in one go
# up to the configured max_download_size, ask only when
# exceeded
r.raw.decode_content = True
size = 0
size_exceeded = False
while True:
buf = r.raw.read(16*1024)
if not size_exceeded:
size += len(buf)
if size > self.max_download_size:
if not self.user_confirm(self.max_download_size_msg_unknown):
r.close()
tmpFile.close()
return ""
else:
size_exceeded = True
if not buf:
break
tmpFile.write(buf)
r.close()
tmpFile.seek(0)
if uri.endswith(".deb"):
# process .deb file
if source_is_cache:
f = uri
else:
f = tmpFile.name
# We could copy the downloaded .deb files to the apt
# cache here but then we'd need to run the script elevated:
# shutil.copy(f, self.apt_cache_path + os.path.basename(uri))
deb = DebPackage(f)
changelog_file = self.get_changelog_from_filelist(deb.filelist)
if changelog_file:
changelog = deb.data_content(changelog_file)
if changelog.startswith('Automatically decompressed:'):
changelog = changelog[29:]
else:
raise ValueError('Malformed Debian package')
elif uri.endswith(".diff.gz"):
# Ubuntu partner repo has .diff.gz files,
# we can extract a changelog from that
data = gzip.open(tmpFile.name, "r").read().decode('utf-8')
additions = data.split("+++")
for addition in additions:
lines = addition.split("\n")
if "/debian/changelog" in lines[0]:
for line in lines[2:]:
if line.startswith("+"):
changelog += f"{line[1:]}\n"
else:
break
if not changelog:
raise ValueError('No changelog in .diff.gz')
else:
# process .tar.xz file
with tarfile.open(fileobj=tmpFile, mode="r:xz") as tar:
changelog_file = self.get_changelog_from_filelist(
[s.name for s in tar.getmembers() if s.type in (b"0", b"2")])
if changelog_file:
changelog = tar.extractfile(changelog_file).read().decode()
else:
raise ValueError('No changelog in source package')
except Exception as e:
_generic_exception_handler(e)
self.exit_on_fail(520)
if 'tmpFile' in vars():
try:
tmpFile.close()
except Exception as e:
_generic_exception_handler(e)
# ALL DONE
return changelog
def parse_package_metadata(self, pkg_name:str):
""" Creates the self.candidate object based on package name=version/release
Wildcard matching is only used for version and release, and only the
first match is processed.
"""
# parse =version declaration
if "=" in pkg_name:
(pkg_name, pkg_version) = pkg_name.split("=", 1)
pkg_release = None
# parse /release declaration (only if no version specified)
elif "/" in pkg_name:
(pkg_name, pkg_release) = pkg_name.split("/", 1)
pkg_version = None
else:
pkg_version = None
pkg_release = None
# check if pkg_name exists
# unlike apt no pattern matching, a single exact match only
if pkg_name in self.apt_cache:
pkg = self.apt_cache[pkg_name]
else:
print(f"E: Unable to locate package {pkg_name}", file=sys.stderr)
self.close(13)
# get package data
_candidate = None
candidate = None
if pkg_release or pkg_version:
match_found = False
for _pkg in pkg.versions:
if pkg_version:
if fnmatch.fnmatch(_pkg.version, pkg_version):
match_found = True
else:
for _origin in _pkg.origins:
if fnmatch.fnmatch(_origin.archive, pkg_release):
match_found = True
if match_found:
_candidate = _pkg
break
if not match_found:
if pkg_release:
print(f'E: Release "{pkg_release}" is unavailable for "{pkg.name}"',
file=sys.stderr)
else:
print(f'E: Version {pkg_version}" is unavailable for "{pkg.name}"',
file=sys.stderr)
self.close(14)
else:
_candidate = pkg.candidate
candidate = _Package(
version = _candidate.version,
name = _candidate.package.name,
fullname = None,
architecture = pkg.architecture,
source_name = _candidate.source_name,
source_version = _candidate.source_version,
uri = _candidate.uri,
filename = os.path.basename(_candidate.filename),
origin = _candidate.origins[0].origin,
component = _candidate.origins[0].component,
downloadable = _candidate.downloadable,
is_installed = _candidate.is_installed,
dependencies = _candidate.dependencies
)
if candidate.is_installed:
candidate.installed_files = pkg.installed_files
candidate.source_version_raw = candidate.source_version
if ":" in candidate.source_version:
candidate.source_version = candidate.source_version.split(":", 1)[1]
return candidate
def check_url(self, url:str, check_size:bool=True, stream:bool=True,
msg:str=None):
""" True if url can be downloaded and fits size requirements """
if _DEBUG: print("Checking:", url)
try:
_r = requests.get(url, stream=stream, timeout=5)
except Exception as e:
_generic_exception_handler(e)
else:
if _r:
if not _r.encoding:
_r.encoding = "utf-8"
length = _r.headers.get("Content-Length")
if length:
_r.length = int(length)
else:
_r.length = 0
if (not check_size or not
(check_size and _r.length > self.max_download_size and not
self.user_confirm(
(self.max_download_size_msg_lc if not msg else msg) %
(_r.length / self.MB))
)):
return _r
if '_r' in vars():
_r.close()
return False
@staticmethod
def close(err:int=0):
""" Exit """
sys.exit(err)
def exit_on_fail(self, err:int=404):
""" Prints error message and calls self.close() """
try:
details = f"Changelog unavailable for {self.candidate.source_name}={self.candidate.source_version_raw}"
except AttributeError:
details = ""
print(f"E: Failed to fetch changelog. {details}", file=sys.stderr)
self.close(err)
@staticmethod
def strtobool (val):
val = val.lower()
if val in ('y', 'yes'):
return True
elif val in ('n', 'no'):
return False
else:
raise ValueError(f"Invalid response value {val}")
def user_confirm(self, q:str):
""" returns bool (always False in non-interactive mode) """
if not self.interactive:
if _DEBUG: print("Maximum size exceeded, skipping in non-interactive mode")
return False
print(f"{q} [y/n] ", end="")
while True:
try:
response = self.strtobool(input())
print("")
return response
except ValueError:
print("Invalid response. Try again [y/n]: ", end="")
except KeyboardInterrupt:
pass
def get_deb_or_tar(self, uri_tar:str=None):
""" Returns request and URI of the preferred source
The choice is made based on availability and size. If .deb is smaller
than comparison_trigger_size, or if check_tar is False, then .deb is
always selected.
"""
comparison_trigger_size = 50000
r_deb = self.check_url(self.candidate.uri, False)
if r_deb:
if uri_tar and r_deb.length > comparison_trigger_size:
# try for .tar.xz
r_tar = self.check_url(uri_tar, False)
# validate and compare sizes
if r_tar and r_tar.length < r_deb.length:
_r = r_tar
r_deb.close()
else:
_r = r_deb
if r_tar:
r_tar.close()
else:
_r = r_deb
if (not _r.length > self.max_download_size or
self.user_confirm(self.max_download_size_msg_lc %
(_r.length / self.MB))
):
return (_r, _r.url)
return (False, "")
def get_changelog_from_filelist(self, filelist:list, local:bool=False):
""" Returns hopefully the correct "changelog" or an empty string.
We should not need to be searching because the debian policy says it
must be at debian/changelog for source packages but not all seem to
adhere to the policy:
https://www.debian.org/doc/debian-policy/ch-source.html#debian-changelog-debian-changelog
"""
files = [s for s in filelist if "changelog" in s.lower()]
if local:
testpath = f"/usr/share/doc/{self.candidate.name}/changelog"
for item in files:
if item.lower().startswith(testpath):
return item
else:
testpath = "debian/changelog"
if testpath in files:
return testpath
testpath = "recipe/debian/changelog"
if testpath in files:
return testpath
testpath = f"usr/share/doc/{self.candidate.name}/changelog"
for item in files:
if item.lower().startswith(testpath):
return item
# no hits in the standard locations, let's try our luck in
# random locations at the risk of getting the wrong file
for item in files:
if os.path.basename(item).lower().startswith("changelog"):
return item
return None
def get_apt_changelog_uri(self, uri_template:str):
""" Returns URI based on provided apt changelog URI template.
Emulates apt's std::string pkgAcqChangelog::URI
The template must contain the @CHANGEPATH@ variable, which will
be expanded to
COMPONENT/SRC/SRCNAME/SRCNAME_SRCVER
Component is omitted for releases without one (= flat-style
repositories).
"""
source_version = self.candidate.source_version
def get_kernel_version_from_meta_package(pkg):
for dependency in pkg.dependencies:
if not dependency.target_versions or not dependency.rawtype == "Depends":
if _DEBUG: print("W: Kernel dependency not found:", dependency)
return None
deppkg = dependency.target_versions[0]
if deppkg.source_name in ("linux", "linux-signed"):
return deppkg.source_version
if deppkg.source_name.startswith("linux-meta"):
_pkg = self.parse_package_metadata(str(deppkg))
return get_kernel_version_from_meta_package(_pkg)
return None
# Ubuntu kernel meta package workaround
if self.candidate.origin == "Ubuntu" and \
self.candidate.source_name.startswith("linux-meta"):
_source_version = get_kernel_version_from_meta_package(self.candidate)
if _source_version:
source_version = _source_version
self.candidate.source_name = "linux"
# Ubuntu signed kernel workaround
if self.candidate.origin == "Ubuntu" and \
self.candidate.source_name == "linux-signed":
self.candidate.source_name = "linux"
# XXX: Debian does not seem to reliably keep changelogs for previous
# (kernel) versions, so should we always look for the latest
# version instead on Debian? apt does not do this but the
# packages.debian.org website shows the latest version in the
# selected archive
# strip epoch
if ":" in source_version:
source_version = source_version.split(":", 1)[1]
# the path is: COMPONENT/SRC/SRCNAME/SRCNAME_SRCVER, e.g.
# main/a/apt/apt_1.1 or contrib/liba/libapt/libapt_2.0
return uri_template.replace('@CHANGEPATH@',
"%(component)s%(source_prefix)s/%(source_name)s/%(source_name)s_%(source_version)s" %
{
"component": self.candidate.component + "/" if \
self.candidate.component and \
self.candidate.component != "" else "",
"source_prefix": self.source_prefix(),
"source_name": self.candidate.source_name,
"source_version": source_version
})
def source_prefix(self, source_name:str=None):
""" Return prefix used for build repository URL """
if not source_name:
source_name = self.candidate.source_name
return source_name[0] if not source_name.startswith("lib") else \
source_name[:4]
def parse_dsc(self, url:str):
""" Returns filename or None """
_r = self.check_url(url, False, False)
if _r:
target = ""
lines = _r.text.split("Files:", 1)[1].split(":", 1)[0].split("-----BEGIN", 1)[0].split("\n")
target = [s.strip() for s in lines if s.strip().lower().endswith('.debian.tar.xz')]
if not target:
target = [s.strip() for s in lines if s.strip().lower().endswith('.diff.gz')]
if not target:
target = [s.strip() for s in lines if s.strip().lower().endswith('.tar.xz')]
# don't even test for .tar.gz, it will be too big compared to the .deb
# if not target:
# target = [s.strip() for s in lines if s.strip().lower().endswith('.tar.gz')]
if target:
return target[0].split()[-1]
elif _DEBUG: print(".dsc parse error for", url)
return None
def get_changelog_uri(self, base_uri:str):
""" Tries to find a changelog in files listed in .dsc, locally cached
packages as well as the remote .deb file
Returns r and uri
"""
uri = None
# XXX: For APT sources we could just read the apt_pkg.SourceRecords()
# directly, if available, which it is not for most users, so
# probably not worth it
target_filename = self.parse_dsc(f"{base_uri}/{self.candidate.source_name}_{self.candidate.source_version}.dsc")
# get .debian.tar.xz or .diff.gz as a priority as the smallest options
if (base_uri and target_filename and (
target_filename.lower().endswith('.debian.tar.xz') or
target_filename.lower().endswith('.diff.gz')
)):
uri = f"{base_uri}/{target_filename}"
target_filename = None
r = self.check_url(uri, msg = self.max_download_size_msg)
else:
r = None
if not r:
# fall back to cached local package
uri = self.apt_cache_path + self.candidate.filename
if not os.path.isfile(uri):
# cache miss, download the full source package or the .deb,
# depending on size and availability
if target_filename:
uri_tar = f"{base_uri}/{target_filename}"
else:
uri_tar = None
r, uri = self.get_deb_or_tar(uri_tar)
return (r, uri)
def _generic_exception_handler(e):
if _DEBUG:
import traceback
print(f"{e.__class__.__name__}: {traceback.format_exc()}\n",
file=sys.stderr)
def drop_cache():
""" Drop the apt cache to free up memory. """
# For some reason this does not free about 11M the first time it is run, and
# additional 21M (32M total) the second time it runs (if the cache had been
# opened again in the meantime). All consecutive runs keep those 32M total
# without additional loss.
# This only affects the freeing of memory, the maximum memory usage is stable.
# It's unclear whether this is a python-apt or a python issue.
if apt_changelog:
apt_changelog.drop_cache()
def get_changelog(pkg_name:str, interactive:bool=False, output:bool=False,
paged_output:bool=False, no_local:bool=False, max_download_size:int=0):
""" Returns changelog for given package name, if any, and if within
size-restrictions
"""
changelog = None
try:
if not apt_changelog:
__init__(interactive)
if int(max_download_size) > 0:
apt_changelog.max_download_size = int(max_download_size)
else:
apt_changelog.max_download_size = apt_changelog.max_download_size_default
changelog = apt_changelog.get_changelog(pkg_name, no_local)
except SystemExit:
if interactive:
raise
except KeyboardInterrupt:
sys.exit(130)
else:
if output:
if not changelog:
# empty changelog
apt_changelog.exit_on_fail(7)
if paged_output:
try:
from pydoc import pager
pager(changelog)
except Exception as e:
_generic_exception_handler(e)
paged_output = False
else:
print(changelog)
return changelog
def print(*args, **kwargs):
try:
return __builtins__.print(*args, **kwargs)
except:
pass
def set_debug(value:bool):
global _DEBUG
_DEBUG = bool(value)
def __init__(interactive:bool=False):
""" Instantiate _AptChangelog to global apt_changelog """
global apt_changelog
apt_changelog = _AptChangelog(interactive)
_DEBUG = False
apt_changelog = None
if __name__ == "__main__":
if "--debug" in sys.argv:
set_debug(True)
sys.argv.remove("--debug")
if "--no-local" in sys.argv:
sys.argv.remove("--no-local")
_no_local=True
else:
_no_local=False
if len(sys.argv) != 2:
print("""\
Usage: apt changelog [options] <package>
Tries to retrieve the changelog of a package and display it through a pager.
By default it displays the changelog for the version that is installed.
However, you can specify the same options as for the install command.
Options:
--no-local
Always retrieve changelogs remotely, where possible. This can be
useful when the locally installed changelog has been truncated.
Changelog lookup may fail for some packages if source repositories
are not enabled""")
sys.exit(1)
else:
isatty = sys.stdin.isatty() and sys.stdout.isatty()
get_changelog(sys.argv[1], interactive=isatty, output=True,
paged_output=isatty, no_local=_no_local)
You can’t perform that action at this time.