Skip to content

Instantly share code, notes, and snippets.

@minimum2scp
Last active November 27, 2022 19:29
Show Gist options
  • Save minimum2scp/5252671 to your computer and use it in GitHub Desktop.
Save minimum2scp/5252671 to your computer and use it in GitHub Desktop.
A web interface for debtree, apt-cache dotty using sinatra (ruby)
#! /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>
&nbsp;
<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