Last active
November 27, 2022 19:29
-
-
Save minimum2scp/5252671 to your computer and use it in GitHub Desktop.
A web interface for debtree, apt-cache dotty
using sinatra (ruby)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /usr/bin/env ruby | |
# -*- coding: utf-8; -*- | |
## | |
## debtree と apt-cache dotty の web インターフェース | |
## (コマンドラインオプション覚えてられない) | |
## | |
## apt-cache dotty については apt-cache(8) を参照 | |
## debtree については以下のサイトを参照 | |
## http://collab-maint.alioth.debian.org/debtree/index.html | |
## | |
## | |
require 'sinatra' | |
require 'tempfile' | |
use Rack::Reloader | |
use Rack::ShowExceptions | |
DEBTREE_OPTIONS = [ | |
{ :short => '-I', | |
:long => '--show-installed', | |
:desc => %Q[Show which packages are installed on the system] }, | |
{ :short => '-R', | |
:long => '--show-rdeps', | |
:desc => %Q[Also show reverse dependencies of the package and any virtual packages it provides] }, | |
{ :short => '-b', | |
:long => '--build-dep', | |
:desc => %Q[Generate a graph showing build dependencies instead of package dependencies] }, | |
{ :long => '--arch', | |
:desc => %Q[Specify the architecture (or 'all') for the build dependency graph (default is the architecture of the system)], | |
:args => true }, | |
{ :short => '-S', | |
:long => '--with-suggests', | |
:desc => %Q[Include suggested packages; dependencies of suggested packages are never included] }, | |
{ :long => '--no-recommends', | |
:desc => %Q[Don't show recommended packages] }, | |
{ :long => '--no-alternatives', | |
:desc => %Q[Only show the first dependency from a set of alternatives (i.e. show what would be installed by default)] }, | |
{ :long => '--no-provides', | |
:desc => %Q[Do not show virtual packages provided by the requested package] }, | |
{ :long => '--max-providers', | |
:desc => %Q[Limit the display of packages that provide a virtual package (default: 3)], | |
:args => true }, | |
{ :long => '--no-versions', | |
:desc => %Q[Don't show the versions for versioned dependencies] }, | |
{ :long => '--no-conflicts', | |
:desc => %Q[Don't show unversioned conflicts] }, | |
{ :short => '-VC', | |
:long => '--versioned-conflicts', | |
:desc => %Q[Include versioned conflicts; by default only unversioned conflicts are shown] }, | |
{ :long => '--max-depth', | |
:desc => %Q[Limit the number of levels of dependencies], | |
:args => true }, | |
{ :long => '--rdeps-depth', | |
:desc => %Q[The maximum number of levels for reverse dependencies (default: 1)], | |
:args => true }, | |
{ :long => '--max-rdeps', | |
:desc => %Q[Limit the display of indirect reverse dependencies (default: 5)], | |
:args => true }, | |
{ :long => '--no-skip', | |
:desc => %Q[Also display packages that are suppressed by default (e.g. libc6)] }, | |
{ :long => '--show-all', | |
:desc => %Q[Generate full dependency tree (recurse for all packages); implies --no-skip] }, | |
{ :short => '-r', | |
:long => '--rotate', | |
:desc => %Q[Draw the graph top-town instead of left-to-right] }, | |
{ :long => '--condense', | |
:desc => %Q[Condense the graph by merging lines (relationships) between packages] }, | |
{ :short => '-q', | |
:long => '--quiet', | |
:desc => %Q[Suppress any informational/warning messages] }, | |
{ :short => '-v', | |
:long => '--verbose', | |
:desc => %Q[Increase verbosity to display debug messages; can be repeated] }, | |
] | |
DEBTREE_GRAPH_TYPES = [ | |
{ :id => 'basic0', | |
:opts => '--no-recommends --no-versions --versioned-conflicts --rotate', | |
:desc => %Q[Graph from apt-cache (for comparison)] }, | |
{ :id => 'basic1', | |
:opts => '--no-recommends --no-alternatives --no-versions', | |
:desc => %Q[Basic graph (only hard dependencies and conflicts)], }, | |
{ :id => 'basic2', | |
:opts => '--no-alternatives --no-versions', | |
:desc => %Q[Basic graph with Recommends], }, | |
{ :id => 'basic3', | |
:opts => '--with-suggests --no-alternatives --no-versions', | |
:desc => %Q[Basic graph with Recommends and Suggests], }, | |
{ :id => 'basic4', | |
:opts => '--no-versions', | |
:desc => %Q[Basic graph with Recommends and showing alternatives], }, | |
{ :id => 'default0', | |
:opts => '', | |
:desc => %Q[Default graph (showing Recommends, alternatives and versions)], }, | |
{ :id => 'default1', | |
:opts => '--with-suggests', | |
:desc => %Q[Default graph with Suggests], }, | |
{ :id => 'default2', | |
:opts => '--with-suggests --versioned-conflicts', | |
:desc => %Q[Default graph with Suggests and versioned Conflicts], }, | |
{ :id => 'default3', | |
:opts => '--rotate', | |
:desc => %Q[Default graph (rotated)], }, | |
{ :id => 'adv0', | |
:opts => '-b --arch=all --no-recommends --no-conflicts', | |
:desc => %Q[Create a build dependency graph], }, | |
{ :id => 'adv1', | |
:opts => '-I -S', | |
:desc => %Q[Visualize what would happen when installing a package], }, | |
{ :id => 'adv2', | |
:opts => '--max-providers=5', | |
:desc => %Q[Dependencies on virtual packages], }, | |
{ :id => 'adv3', | |
:opts => '-I --rdeps-depth=3', | |
:desc => %Q[Reverse dependencies], }, | |
] | |
FORMAT_TYPES = [ | |
{ :id => "svg", :content_type => "image/svg+xml", :binary => false, :default => true }, | |
{ :id => "png", :content_type => "image/png", :binary => true, }, | |
#{ :id => "dia", :content_type => "application/octet-stream", :binary => true, :set_filename => true }, | |
{ :id => "ps", :content_type => "application/postscript", :binary => false, :set_filename => true }, | |
] | |
class InvalidPackageName < StandardError; end | |
def valid_package_name?(package) | |
/\A[a-z0-9.-]+\Z/ =~ package | |
end | |
def opts2json(opts) | |
json_ary = [] | |
opts.split(/\s+/).each do |w| | |
case w | |
when /^-[a-zA-Z]$/ | |
long = DEBTREE_OPTIONS.find{|o| o[:short] == w}[:long].sub(/--/,'').gsub(/-/,"_") | |
json_ary << "#{long}: true" | |
when /^--[^=]+$/ | |
json_ary << w.sub(/--/,"").gsub(/-/,"_") + ": true" | |
when /^--[^=]+=.+$/ | |
k,v = w.split(/=/) | |
json_ary << k.sub(/--/, "").gsub(/-/,"_") + ":" + "'#{v}'" | |
end | |
end | |
return "{" + json_ary.join(",") + "}" | |
end | |
def debtree(package,fmt,params) | |
header = nil | |
body = nil | |
file = Tempfile.new( File.basename(__FILE__) ) | |
file.close | |
opts = "" | |
if params[:opts] | |
params[:opts].each do |k,v| | |
o = DEBTREE_OPTIONS.find{|o2| o2[:long] == k.gsub(/_/,"-").sub(/^/,"--") } | |
if v == "on" && o | |
if o[:args] | |
a = params[:optargs][k] | |
opts << o[:long] + "=" + a << " " if a && a.size>0 | |
else | |
opts << o[:long] << " " | |
end | |
end | |
end | |
end | |
cmd = "debtree #{opts} #{package} | dot -T #{fmt} -o #{file.path}" | |
warn "[DEBUG] params: #{params.inspect} -> opts: #{opts}" | |
warn "[DEBUG] cmd: #{cmd}" | |
fmt_ref = FORMAT_TYPES.find{|f| f[:id] == fmt} | |
raise "unknown format: #{fmt}" unless fmt_ref | |
system cmd | |
body = fmt_ref[:binary] ? [ File.read(file.path) ] : File.readlines(file.path) | |
file.unlink | |
header = { "Content-Type" => fmt_ref[:content_type] } | |
header["Content-Disposition"] = "attachment; filename=\"#{package}.#{fmt}\"" if fmt_ref[:set_filename] | |
[header, body] | |
end | |
def dotty(packages,fmt) | |
header = nil | |
body = nil | |
file = Tempfile.new( File.basename(__FILE__) ) | |
file.close | |
## NOTE: APT::Cache::GivenOnly なしでは巨大すぎるグラフが生成されてしまう (apt-cache(1) 参照) | |
opts = "-o APT::Cache::GivenOnly=yes" | |
cmd = "apt-cache dotty #{opts} #{packages.join(' ')} | dot -T #{fmt} -o #{file.path}" | |
warn "[DEBUG] cmd: #{cmd}" | |
fmt_ref = FORMAT_TYPES.find{|f| f[:id] == fmt} | |
raise "unknown format: #{fmt}" unless fmt_ref | |
system cmd | |
body = fmt_ref[:binary] ? [ File.read(file.path) ] : File.readlines(file.path) | |
file.unlink | |
header = { "Content-Type" => fmt_ref[:content_type] } | |
header["Content-Disposition"] = "attachment; filename=\"#{packages.join('_')}.#{fmt}\"" if fmt_ref[:set_filename] | |
[header, body] | |
end | |
helpers do | |
include Rack::Utils | |
alias_method :h, :escape_html | |
end | |
get '/' do | |
redirect '/dotty' | |
end | |
get '/debtree' do | |
erb :debtree_form | |
end | |
post '/debtree' do | |
package = params[:package].strip | |
fmt = params[:fmt] | |
url = "/debtree/#{package}.#{fmt}" | |
query = '' | |
if params[:opts] | |
query << [ | |
params[:opts].map{|k,v| "opts[#{k}]=#{v}" }, | |
params[:optargs].select{|k,v| params[:opts][k] }.map{|k,v| "optargs[#{k}]=#{v}" } | |
].flatten.join("&") | |
end | |
redirect query.size > 0 ? url + "?" + query : url | |
end | |
get '/debtree/:package.:fmt' do | |
package = params[:package] | |
fmt = params[:fmt] | |
unless valid_package_name?(package) | |
raise InvalidPackageName, "invalid package name #{package}" | |
end | |
header, body = debtree(package, fmt, params) | |
[ 200, header, body ] | |
end | |
get '/dotty' do | |
erb :dotty_form | |
end | |
post '/dotty' do | |
fmt = params[:fmt] | |
packages = params[:packages].split(/\s+/) | |
url = "/dotty/#{packages.join("_")}.#{fmt}" | |
redirect packages.empty? ? '/dotty' : url | |
end | |
get '/dotty/:packages.:fmt' do | |
fmt = params[:fmt] | |
packages = params[:packages].split('_').map{|package| package.strip } | |
packages.each do |package| | |
unless valid_package_name?(package) | |
raise InvalidPackageName, "invalid package name #{package}" | |
end | |
end | |
header, body = dotty(packages, fmt) | |
[ 200, header, body ] | |
end | |
template :debtree_form do | |
%Q[ | |
<html> | |
<head> | |
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> | |
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script> | |
<script type="text/javascript"> | |
function set_options(new_options){ | |
$("input:checkbox[name^=opts]").each( function(idx,elem){ $(elem).attr("checked", false) } ); | |
$("input:text[name^=optargs]").each( function(idx,elem){ $(elem).val("") }); | |
$.each(new_options, function(opt1,optarg1){ | |
$('input:checkbox[name="opts[' + opt1 + ']"]').attr("checked", true); | |
$('input:text[name="optargs[' + opt1 + ']"]').val(optarg1); | |
}); | |
} | |
</script> | |
</head> | |
<body> | |
<p><a href="/dotty">dotty</a></p> | |
<hr> | |
<form id="graph" method="post" action="/debtree"> | |
<table> | |
<tr> | |
<td> package: </td> | |
<td> <input type="input" name="package" size="60"> <input type="submit" value="submit"> </td> | |
</tr> | |
<tr> | |
<td> fmt: </td> | |
<td> | |
<% FORMAT_TYPES.each do |f| %> | |
<input type="radio" name="fmt" value="<%=h f[:id] %>" <%=h f[:default] ? "checked" : ""%>><%=h f[:id] %> | |
<% end %> | |
</td> | |
</tr> | |
<tr> | |
<td>options:</td> | |
<td></td> | |
</tr> | |
<% DEBTREE_OPTIONS.each do |o| %> | |
<% param = o[:long].sub(/^--/,'opts[').sub(/$/,']').gsub(/-/, '_') %> | |
<% param_arg = param.sub(/^opts/, 'optargs') %> | |
<tr> | |
<td> | |
| |
<input type="checkbox" name="<%=h param %>" value="on"> | |
<%=h o[:long] %> <%if o[:short] %>| <%=h o[:short] %><% end %> | |
<% if o[:args] %> | |
= <input type="text" name="<%=h param_arg %>" size="4"> | |
<% end %> | |
</td> | |
<td> <%=h o[:desc] %> </td> | |
</tr> | |
<% end %> | |
</table> | |
</form> | |
<hr> | |
<p>example option set:</p> | |
<table> | |
<% DEBTREE_GRAPH_TYPES.each do |g| %> | |
<tr> | |
<td> <button onclick="set_options( <%=h opts2json(g[:opts]) %> ); return false"><%=h g[:id] %></button> | |
</td> | |
<td> | |
<%=h g[:desc] %> | |
<% if g[:opts].size > 0 %> | |
<br> <%=h g[:opts] %> | |
<% end %> | |
</td> | |
</tr> | |
<% end %> | |
</table> | |
</body> | |
</html> | |
] | |
end | |
template :dotty_form do | |
%Q[ | |
<html> | |
<head> | |
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> | |
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script> | |
</head> | |
<body> | |
<p><a href="/debtree">debtree</a></p> | |
<hr> | |
<form id="graph" method="post" action="/dotty"> | |
<table> | |
<tr> | |
<td> packages (one or more): </td> | |
<td> <input type="input" name="packages" size="60"> <input type="submit" value="submit"> </td> | |
</tr> | |
<tr> | |
<td> fmt: </td> | |
<td> | |
<% FORMAT_TYPES.each do |f| %> | |
<input type="radio" name="fmt" value="<%=h f[:id] %>" <%=h f[:default] ? "checked" : ""%>><%=h f[:id] %> | |
<% end %> | |
</td> | |
</tr> | |
</table> | |
</form> | |
</body> | |
</html> | |
] | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment