Add source-download command (fate#313445)
authorMichael Andres <ma@suse.de>
Fri, 18 Jan 2013 16:09:28 +0000 (17:09 +0100)
committerMichael Andres <ma@suse.de>
Fri, 18 Jan 2013 19:02:19 +0000 (20:02 +0100)
doc/zypper.8
src/CMakeLists.txt
src/Command.cc
src/Command.h
src/Zypper.cc
src/source-download.cc [new file with mode: 0644]
src/source-download.h [new file with mode: 0644]

index 1caf96c..c820f89 100644 (file)
@@ -1510,6 +1510,26 @@ This command can be useful for companies redistributiong a custom
 distribution (like appliances) to figure out what licenses they are bound by.
 
 .TP
+.B source-download
+Download source rpms for all installed packages to a local directory.
+
+.TP
+.I \-d, \-\-directory <dir>
+Download all source rpms to this directory. Default is \fB/var/cache/zypper/source-download\fR.
+
+.TP
+.I \-\-delete
+Delete extraneous source rpms in the local directory. This is the default.
+
+.TP
+.I \-\-no-delete
+Do not delete extraneous source rpms.
+
+.TP
+.I \-\-status
+Don't download any source rpms, but show which source rpms are missing or extraneous.
+
+.TP
 .B ps
 After each upgrade or removal of packages, there may be running processes
 on the system which then use files meanwhile deleted by the upgrade.
index c58e193..0d48079 100644 (file)
@@ -19,6 +19,7 @@ SET (zypper_HEADERS
   Table.h
   locks.h
   update.h
+  source-download.h
   solve-commit.h
   PackageArgs.h
   SolverRequester.h
@@ -40,6 +41,7 @@ SET( zypper_SRCS
   Table.cc
   locks.cc
   update.cc
+  source-download.cc
   solve-commit.cc
   PackageArgs.cc
   RequestFeedback.cc
index a6894d7..1a96cc3 100644 (file)
@@ -78,6 +78,7 @@ namespace
       _T( VERSION_CMP_e )      | "versioncmp"          | "vcmp";
       _T( LICENSES_e )         | "licenses";
       _T( PS_e )               | "ps";
+      _T( SOURCE_DOWNLOAD_e )  | "source-download";
 
       _T( HELP_e )             | "help"                | "?";
       _T( SHELL_e )            | "shell"               | "sh";
@@ -151,6 +152,7 @@ DEF_ZYPPER_COMMAND( TARGET_OS );
 DEF_ZYPPER_COMMAND( VERSION_CMP );
 DEF_ZYPPER_COMMAND( LICENSES );
 DEF_ZYPPER_COMMAND( PS );
+DEF_ZYPPER_COMMAND( SOURCE_DOWNLOAD );
 
 DEF_ZYPPER_COMMAND( HELP );
 DEF_ZYPPER_COMMAND( SHELL );
index 163c808..5bc19cc 100644 (file)
@@ -68,6 +68,7 @@ struct ZypperCommand
   static const ZypperCommand VERSION_CMP;
   static const ZypperCommand LICENSES;
   static const ZypperCommand PS;
+  static const ZypperCommand SOURCE_DOWNLOAD;
 
   static const ZypperCommand HELP;
   static const ZypperCommand SHELL;
@@ -140,6 +141,7 @@ struct ZypperCommand
     VERSION_CMP_e,
     LICENSES_e,
     PS_e,
+    SOURCE_DOWNLOAD_e,
 
     HELP_e,
     SHELL_e,
index 4c9af93..e039aec 100644 (file)
@@ -54,6 +54,7 @@
 #include "locks.h"
 #include "search.h"
 #include "info.h"
+#include "source-download.h"
 
 #include "output/OutNormal.h"
 #include "output/OutXML.h"
@@ -269,6 +270,8 @@ void print_main_help(Zypper & zypper)
     "\ttargetos, tos\t\tPrint the target operating system ID string.\n"
     "\tlicenses\t\tPrint report about licenses and EULAs of\n"
     "\t\t\t\tinstalled packages.\n"
+    "\tsource-download\t\tDownload source rpms for all installed packages\n"
+    "\t\t\t\tto a local directory.\n"
   );
 
   static string help_usage = _(
@@ -2381,6 +2384,42 @@ void Zypper::processCommandOptions()
   }
 
 
+  case ZypperCommand::SOURCE_DOWNLOAD_e:
+  {
+    shared_ptr<SourceDownloadOptions> myOpts( new SourceDownloadOptions() );
+    _commandOptions = myOpts;
+    static struct option options[] =
+    {
+      {"help",                 no_argument, 0, 'h'},
+      {"directory",            required_argument, 0, 'd'},
+//       {"manifest",          no_argument, &myOpts->_manifest, 1},
+//       {"no-manifest",               no_argument, &myOpts->_manifest, 0},
+      {"delete",               no_argument, &myOpts->_delete, 1},
+      {"no-delete",            no_argument, &myOpts->_delete, 0},
+      {"status",               no_argument, &myOpts->_dryrun, 1},
+      {0, 0, 0, 0}
+    };
+    specific_options = options;
+    _command_help = _(
+      "source-download\n"
+      "\n"
+      "Download source rpms for all installed packages to a local directory.\n"
+      "\n"
+      "  Command options:\n"
+      "-d, --directory <dir>\n"
+      "                     Download all source rpms to this directory.\n"
+      "                     Default: /var/cache/zypper/source-download\n"
+      "--delete             Delete extraneous source rpms in the local directory.\n"
+      "--no-delete          Do not delete extraneous source rpms.\n"
+      "--status             Don't download any source rpms,\n"
+      "                     but show which source rpms are missing or extraneous.\n"
+    );
+//       "--manifest           Write MANIFEST of packages and coresponding source rpms.\n"
+//       "--no-manifest        Do not write MANIFEST.\n"
+    break;
+  }
+
+
   case ZypperCommand::SHELL_QUIT_e:
   {
     static struct option quit_options[] = {
@@ -4190,8 +4229,7 @@ void Zypper::doCommand()
     else
     {
       SolverRequester::Options sropts;
-      if (copts.find("force") !=
-copts.end())
+      if (copts.find("force") != copts.end())
         sropts.force = true;
       sropts.best_effort = best_effort;
       sropts.skip_interactive = skip_interactive; // bcn #647214
@@ -4662,6 +4700,29 @@ copts.end())
     break;
   }
 
+  case ZypperCommand::SOURCE_DOWNLOAD_e:
+  {
+    if (runningHelp()) { out().info(_command_help, Out::QUIET); return; }
+
+    if (!_arguments.empty())
+    {
+      report_too_many_arguments(_command_help);
+      setExitCode(ZYPPER_EXIT_ERR_INVALID_ARGS);
+      return;
+    }
+
+    shared_ptr<SourceDownloadOptions> myOpts( assertCommandOptions<SourceDownloadOptions>() );
+
+    if ( _copts.count( "directory" ) )
+      myOpts->_directory = _copts["directory"].back(); // last wins
+
+    if ( _copts.count( "dry-run" ) )
+      myOpts->_dryrun = true;
+
+    sourceDownload( *this );
+
+    break;
+  }
 
   // -----------------------------( shell )------------------------------------
 
diff --git a/src/source-download.cc b/src/source-download.cc
new file mode 100644 (file)
index 0000000..1bb17e3
--- /dev/null
@@ -0,0 +1,575 @@
+/*---------------------------------------------------------------------------*\
+                          ____  _ _ __ _ __  ___ _ _
+                         |_ / || | '_ \ '_ \/ -_) '_|
+                         /__|\_, | .__/ .__/\___|_|
+                             |__/|_|  |_|
+\*---------------------------------------------------------------------------*/
+
+#include <iostream>
+
+#include <zypp/base/LogTools.h>
+#include <zypp/ResPool.h>
+#include <zypp/Package.h>
+#include <zypp/SrcPackage.h>
+#include <zypp/target/rpm/RpmHeader.h>
+#include <zypp/repo/SrcPackageProvider.h>
+#include <zypp/ZYppCallbacks.h>
+
+#include "Zypper.h"
+#include "Table.h"
+#include "source-download.h"
+
+///////////////////////////////////////////////////////////////////
+// SourceDownloadOptions
+///////////////////////////////////////////////////////////////////
+
+const Pathname SourceDownloadOptions::_defaultDirectory( "/var/cache/zypper/source-download" );
+const std::string SourceDownloadOptions::_manifestName( "MANIFEST" );
+
+inline std::ostream & operator<<( std::ostream & str, const SourceDownloadOptions & obj )
+{
+  return str << boost::format( "{%1%|%2%%3%}" )
+             % obj._directory
+//           % (obj._manifest ? 'M' : 'm' )
+             % (obj._delete ? 'D' : 'd' )
+             % (obj._dryrun ? "(dry-run)" : "" );
+}
+
+///////////////////////////////////////////////////////////////////
+namespace
+{
+
+  ///////////////////////////////////////////////////////////////////
+  /// \class SourceDownloadImpl
+  /// \brief Implementation of source-download commands.
+  ///////////////////////////////////////////////////////////////////
+  class SourceDownloadImpl
+  {
+  public:
+    SourceDownloadImpl( Zypper & zypper_r )
+    : _zypper( zypper_r )
+    , _options( _zypper.commandOptionsAs<SourceDownloadOptions>() )
+    , _dnlDir( Pathname::assertprefix( _zypper.globalOpts().root_dir, _options->_directory ) )
+    { MIL << "SourceDownload " << _options << endl; }
+
+  public:
+
+    ///////////////////////////////////////////////////////////////////
+    /// \class SourceDownloadImpl::SourcePkg
+    /// \brief A Manifest entry.
+    ///////////////////////////////////////////////////////////////////
+    /** Manifest entry */
+    struct SourcePkg
+    {
+      enum Status
+      {
+       S_EMPTY,        //< !needed && !downloaded
+       S_SUPERFLUOUS,  //< !needed &&  downloaded
+       S_MISSING,      //<  needed && !downloaded
+       S_OK,           //<  needed &&  downloaded
+      };
+
+      SourcePkg( const std::string & longname_r = std::string() )
+      : _longname( longname_r ) {}
+
+      bool needed() const
+      { return !_packages.empty(); }
+
+      bool downloaded() const
+      { return !_localFile.empty(); }
+
+      Status status() const
+      {
+       if ( needed() )
+         return downloaded() ? S_OK : S_MISSING;
+       return downloaded() ? S_SUPERFLUOUS : S_EMPTY;
+      }
+
+      PoolItem lookupSrcPackage()
+      {
+       if ( ! _srcPackage )
+       {
+         ResPool pool( ResPool::instance() );
+         Package::constPtr pkg( _packages.front()->asKind<Package>() );
+         for_( it, pool.byIdentBegin<SrcPackage>( pkg->sourcePkgName() ), pool.byIdentEnd<SrcPackage>( pkg->sourcePkgName() ) )
+         {
+           if ( (*it)->edition() == pkg->sourcePkgEdition() )
+           {
+             _srcPackage = *it;
+             break;
+           }
+         }
+       }
+       return _srcPackage;
+      }
+
+      std::string _longname;   //< Key: name-version-release.type
+      std::string _localFile;  //< name of srpm if downloaded
+      PoolItem _srcPackage;    //< available SrcPackage providning the srpm
+      std::vector<PoolItem> _packages; //< installed Packages built from this srpm
+
+      static std::string makeLongname( const std::string & name_r, Edition edition_r, bool nosrc_r )
+      { return str::Str() << name_r << '-' << edition_r << '.' << (nosrc_r ? "nosrc" : "src"); }
+   };
+    ///////////////////////////////////////////////////////////////////
+
+    ///////////////////////////////////////////////////////////////////
+    /// \class SourceDownloadImpl::Manifest
+    /// \brief A set of SourcePkg
+    ///////////////////////////////////////////////////////////////////
+    struct Manifest : public std::map<std::string, SourceDownloadImpl::SourcePkg>
+    {
+      typedef std::map<SourcePkg::Status, DefaultIntegral<unsigned,0U> > StatusMap;
+
+      /** Return \ref SourcePkg for \a key_r (assert \c SourcePkg::_longname is set) */
+      SourcePkg & get( const std::string & key_r )
+      {
+       SourcePkg & ret( operator[]( key_r ) );
+       if ( ret._longname.empty() )
+         ret._longname = key_r;
+       return ret;
+      }
+
+      void updateStatus( StatusMap & status_r ) const
+      {
+       StatusMap status;
+       for_( it, begin(), end() )
+       {
+         const SourcePkg & src( it->second );
+         ++status[src.status()];
+       }
+       status_r.swap( status );
+      }
+    };
+    ///////////////////////////////////////////////////////////////////
+
+    ///////////////////////////////////////////////////////////////////
+    /// \class SourceDownloadImpl::DownloadProgress
+    /// \brief Listen on media::DownloadProgressReport to feed a ProgressBar.
+    ///
+    /// Connect to media::DownloadProgressReport to feed a ProgressBar, but forward
+    /// callbacks to any original receiver.
+    /// \todo Probably move this as helper class to \ref Out.
+    ///////////////////////////////////////////////////////////////////
+    struct DownloadProgress : public callback::ReceiveReport<media::DownloadProgressReport>
+    {
+      DownloadProgress( Out::ProgressBar & progressbar_r )
+      : _progressbar( &progressbar_r )
+      , _oldReceiver( Distributor::instance().getReceiver() )
+      {
+       connect();
+      }
+
+      ~DownloadProgress()
+      {
+       if ( _oldReceiver )
+         Distributor::instance().setReceiver( *_oldReceiver );
+       else
+         Distributor::instance().noReceiver();
+      }
+
+      virtual void start( const Url & file, Pathname localfile )
+      {
+       (*_progressbar)->range( 100 );  // we'll receive %
+
+       if ( _oldReceiver )
+         _oldReceiver->start( file, localfile );
+      }
+
+      virtual bool progress( int value, const Url & file, double dbps_avg = -1, double dbps_current = -1 )
+      {
+       (*_progressbar)->set( value );
+
+       if ( _oldReceiver )
+         return _oldReceiver->progress( value, file, dbps_avg, dbps_current );
+       return true;
+      }
+
+      virtual Action problem( const Url & file, Error error, const std::string & description )
+      {
+       ERR << description << endl;
+
+       if ( _oldReceiver )
+         return _oldReceiver->problem( file, error, description );
+       return Receiver::problem( file, error, description );
+      }
+
+      virtual void finish( const Url & file, Error error, const std::string & reason )
+      {
+       if ( error == NO_ERROR )
+         (*_progressbar)->toMax();
+       else
+       {
+         ERR << reason << endl;
+         _progressbar->error();
+       }
+
+       if ( _oldReceiver )
+         _oldReceiver->finish( file, error, reason );
+      }
+
+    private:
+      Out::ProgressBar * _progressbar;
+      Receiver * _oldReceiver;
+    };
+    ///////////////////////////////////////////////////////////////////
+
+  public:
+    void sourceDownload();
+
+  private:
+    /** Startup and build manifest. */
+    void buildManifest();
+
+    std::ostream & dumpManifestSumary( std::ostream & str, Manifest::StatusMap & status );
+    std::ostream & dumpManifestTable( std::ostream & str );
+
+  private:
+    Zypper & _zypper;                          //< my Zypper
+    shared_ptr<SourceDownloadOptions> _options;        //< my Options
+  private:
+    Pathname _dnlDir;  //< download directory (incl. root prefix)
+    Manifest _manifest;
+    DefaultIntegral<unsigned,0U> _installedPkgCount;
+  };
+  ///////////////////////////////////////////////////////////////////
+
+  /** \relates SourceDownloadImpl::SourcePkg::Status String representation */
+  inline std::string asString( SourceDownloadImpl::SourcePkg::Status obj )
+  {
+    switch ( obj )
+    {
+      case SourceDownloadImpl::SourcePkg::S_EMPTY:     return "?"; break;
+      case SourceDownloadImpl::SourcePkg::S_SUPERFLUOUS:return "+"; break;
+      case SourceDownloadImpl::SourcePkg::S_MISSING:   return "-"; break;
+      case SourceDownloadImpl::SourcePkg::S_OK:                return " "; break;
+    }
+    return "?";
+  }
+
+  /** \relates SourceDownloadImpl::SourcePkg::Status Stream output */
+  inline std::ostream & operator<<( std::ostream & str, SourceDownloadImpl::SourcePkg::Status obj )
+  { return str << asString( obj ); }
+
+  /** \relates SourceDownloadImpl::SourcePkg Stream output */
+  inline std::ostream & operator<<( std::ostream & str, const SourceDownloadImpl::SourcePkg & obj )
+  {
+    str << obj.status() << " " << obj._longname << endl;
+    str << "  spkg:  " << obj._srcPackage << endl;
+    str << "  pkgs:  " << obj._packages.size();
+    return str;
+  }
+
+  ///////////////////////////////////////////////////////////////////
+  /// class SourceDownloadImpl
+  ///////////////////////////////////////////////////////////////////
+
+  void SourceDownloadImpl::buildManifest()
+  {
+    PathInfo pi( _dnlDir );
+    if ( pi.error() == ENOENT )
+    {
+      // try to create it on the fly
+      filesystem::assert_dir( pi.path() );
+      pi();
+    }
+    DBG << "Download directory: " << pi << endl;
+
+    if ( ! pi.isDir() )
+    {
+      ERR << "Can't create or access download directory" << endl;
+      throw( Out::Error( ZYPPER_EXIT_ERR_BUG,
+                        boost::format(_("Can't create or access download directory '%s'.")) % pi.asString(),
+                        Errno( pi.error() ).asString() ) );
+      return;
+    }
+
+    if ( ! ( pi.userMayR() && ( _options->_dryrun || pi.userMayW() ) ) )
+    {
+      ERR << "Download directory is not readable." << endl;
+      throw( Out::Error( ZYPPER_EXIT_ERR_PRIVILEGES,
+                        boost::format(_("Insufficient privileges to use download directory '%s'.")) % pi.asString() ) );
+      return;
+    }
+
+    _zypper.out().info(
+      boost::format(_("Using download directory at '%s'.")) % pi.asString()
+    );
+
+    // scan download directory to manifest
+    {
+      std::list<std::string> todolist;
+      int ret = readdir( todolist, pi.path(), /*dots*/false );
+      if ( ret != 0 )
+      {
+       ERR << "Failed to read download directory." << endl;
+       throw( Out::Error( ZYPPER_EXIT_ERR_BUG,
+                          _("Failed to read download directory"),
+                          Errno().asString() ) );
+       return;
+      }
+
+      Out::ProgressBar report( _zypper.out(), _("Scanning download directory") );
+      report->range( todolist.size() );
+      for ( const auto & file : todolist )
+      {
+       report->incr(); // fast enough to count in advance.
+
+       if ( file == _options->_manifestName )
+         continue;
+
+       using target::rpm::RpmHeader;
+       Pathname path( pi.path() / file );
+       RpmHeader::constPtr pkg( RpmHeader::readPackage( path, RpmHeader::NOVERIFY ) );
+
+       if ( ! ( pkg && pkg->isSrc() ) )
+         continue;
+
+       SourcePkg & spkg( _manifest.get( SourcePkg::makeLongname( pkg->tag_name(), pkg->tag_edition(), pkg->isNosrc() ) ) );
+       spkg._localFile = file;
+      }
+    }
+
+    // scan installed packages to manifest
+    {
+      if ( _zypper.defaultLoadSystem( _options->_dryrun ? Zypper::NO_REPOS : Zypper::LoadSystemFlags() ) != ZYPPER_EXIT_OK )
+      {
+       ERR << "Startup returns " << _zypper.exitCode() << endl;
+       throw( Out::Error(_("Failed to read download directory"),
+                         Errno().asString() ) );
+      }
+
+      Out::ProgressBar report( _zypper.out(), _("Scanning installed packages") );
+      ResPool pool( ResPool::instance() );
+      for_( it, pool.byKindBegin<Package>(), pool.byKindEnd<Package>() )
+      {
+       if ( ! it->status().isInstalled() )
+         continue;
+
+       SourcePkg & spkg( _manifest.get( (*it)->asKind<Package>()->sourcePkgLongName() ) );
+       spkg._packages.push_back( *it );
+       ++_installedPkgCount;   // on the fly count installed packages
+      }
+    }
+  }
+
+  std::ostream & SourceDownloadImpl::dumpManifestSumary( std::ostream & str, Manifest::StatusMap & status )
+  {
+    {
+      Table t;
+      t.lineStyle( none );
+      { TableRow tr; tr << _("Installed packages:")
+                        << str::numstring( _installedPkgCount, 5 )
+                        ; t << tr; }
+      { TableRow tr; tr << _("Required source packages:")
+                        << str::numstring( _manifest.size() - status[SourcePkg::S_SUPERFLUOUS] - status[SourcePkg::S_EMPTY], 5 )
+                        ; t << tr; }
+      str << t << endl;
+    }
+    {
+      Table t;
+      t.lineStyle( none );
+      { TableRow tr; tr << '[' + asString( SourcePkg::S_OK ) + ']'
+                        << _("Required source packages available in download directory:")
+                        << str::numstring( status[SourcePkg::S_OK], 5 );
+                        t << tr; }
+      { TableRow tr; tr << '[' + asString( SourcePkg::S_MISSING ) + ']'
+                        << _("Required source packages to be downloaded:")
+                        << str::numstring( status[SourcePkg::S_MISSING], 5 );
+                        t << tr; }
+      { TableRow tr; tr << '[' + asString( SourcePkg::S_SUPERFLUOUS ) + ']'
+                        << _("Superfluous source packages in download directory:")
+                        << str::numstring( status[SourcePkg::S_SUPERFLUOUS], 5 );
+                        t << tr; }
+      str << t << endl;
+    }
+    return str;
+  }
+
+
+  std::ostream & SourceDownloadImpl::dumpManifestTable( std::ostream & str )
+  {
+    Table t;
+    TableHeader th;
+    // translators: table headers
+    th << "#" << _("Source package") << _("Installed package");
+    t << th;
+
+    for ( const auto & item : _manifest )
+    {
+      const SourcePkg & spkg( item.second );
+      std::string l1( asString( spkg.status() ) );
+      std::string l2( spkg._longname );
+      if ( spkg._packages.empty() )
+      {
+       TableRow tr;
+       tr << l1 << l2 << "-----";
+       t << tr;
+      }
+      else
+      {
+       for ( const auto & pkg : spkg._packages )
+       {
+         TableRow tr;
+         tr << l1 << l2 << pkg.satSolvable();
+         t << tr;
+         if ( ! l1.empty() )
+         {
+           l1.clear();
+           l2 = "  --\"--";
+         }
+       }
+      }
+    }
+    str << t << endl;
+    return str;
+  }
+
+
+  void SourceDownloadImpl::sourceDownload()
+  {
+    buildManifest();
+    Manifest::StatusMap status;
+    _manifest.updateStatus( status );
+
+    cout << endl;
+    dumpManifestSumary( cout, status );
+
+    if ( _options->_dryrun )
+    {
+      if ( _zypper.out().verbosity() > Out::NORMAL )
+      {
+       dumpManifestTable( cout );
+      }
+      else
+      {
+       _zypper.out().info(_("Use '--verbose' option for a full list of required source packages.") );
+      }
+      return;  // --> dry run ends here.
+    }
+
+    // delete superfluous source packages
+
+    if ( status[SourcePkg::S_SUPERFLUOUS] && _options->_delete )
+    {
+      Out::ProgressBar report( _zypper.out(), _("Deleting superfluous source packages") );
+      report->range( status[SourcePkg::S_SUPERFLUOUS] );
+      for ( auto & item : _manifest )
+      {
+       SourcePkg & spkg( item.second );
+       if ( spkg.status() != SourcePkg::S_SUPERFLUOUS )
+         continue;
+
+       int res = filesystem::unlink( _dnlDir / spkg._localFile );
+       if ( res != 0 )
+       {
+         throw( Out::Error( boost::format(_("Failed to remove source package '%s'") ) % (_dnlDir / spkg._localFile),
+                           Errno().asString() ) );
+       }
+       MIL << spkg << endl;
+       spkg._localFile.clear();
+       DBG << spkg << endl;
+       report->incr();
+      }
+    }
+    else
+    {
+      std::string msg(_("No superfluous source packages to delete.") );
+      if ( status[SourcePkg::S_SUPERFLUOUS] )
+       msg += " (--no-delete)";
+      _zypper.out().info( msg );
+    }
+
+    // download missing packages
+
+    if ( status[SourcePkg::S_MISSING] )
+    {
+      _zypper.out().info(_("Downloading required source packages...") );
+      repo::RepoMediaAccess access;
+      repo::SrcPackageProvider prov( access );
+      unsigned current = 0;
+      for ( auto & item : _manifest )
+      {
+       SourcePkg & spkg( item.second );
+       if ( spkg.status() != SourcePkg::S_MISSING )
+         continue;
+       ++current;
+
+       try
+       {
+         Out::ProgressBar report( _zypper.out(), spkg._longname, current, status[SourcePkg::S_MISSING] );
+
+         if ( ! spkg.lookupSrcPackage() )
+         {
+           report.error();
+           throw( Out::Error( ZYPPER_EXIT_ERR_BUG,
+                              boost::format(_("Source package '%s' is not provided by any repository.") ) % spkg._longname ) );
+         }
+         report.print( str::form( "%s (%s)",  spkg._longname.c_str(), spkg._srcPackage->repository().name().c_str() ) );
+         MIL << spkg._srcPackage << endl;
+
+         ManagedFile localfile;
+         {
+           report.error(); // error if provideSrcPackage throws
+           DownloadProgress redirect( report );
+           localfile = prov.provideSrcPackage( spkg._srcPackage->asKind<SrcPackage>() );
+           DBG << localfile << endl;
+           report.error( false );
+         }
+
+         if ( filesystem::hardlinkCopy( localfile, _dnlDir / (spkg._longname+".rpm") ) != 0 )
+         {
+           ERR << "Can't hardlink/copy " << localfile << " to " <<  (_dnlDir / spkg._longname) << endl;
+           report.error();
+           throw( Out::Error( ZYPPER_EXIT_ERR_BUG,
+                              boost::format(_("Error downloading source package '%s'.") ) % spkg._longname ),
+                              Errno().asString() );
+         }
+         spkg._localFile = spkg._longname;
+       }
+       catch ( const Out::Error & error_r )
+       {
+         error_r.report( _zypper );
+       }
+       catch ( const Exception & exp )
+       {
+         // TODO: Need class Out::Error support for exceptions
+         ERR << exp << endl;
+         _zypper.out().error( exp,
+                              boost::str( boost::format(_("Error downloading source package '%s'.") ) % spkg._longname ) );
+
+         //throw( Out::Error( ZYPPER_EXIT_ERR_BUG ) );
+       }
+
+       if ( _zypper.exitRequested() )
+         throw( Out::Error( ZYPPER_EXIT_ON_SIGNAL ) );
+      }
+    }
+    else
+    {
+      _zypper.out().info(_("No source packages to download.") );
+    }
+
+    // finished
+    cout << endl;
+    if ( _zypper.exitCode() != ZYPPER_EXIT_OK )
+      _zypper.out().info(_("Finished with error.") );
+    else
+      _zypper.out().info(_("Done.") );
+  }
+
+} // namespace
+///////////////////////////////////////////////////////////////////
+
+int sourceDownload( Zypper & zypper_r )
+{
+  try
+  {
+    SourceDownloadImpl( zypper_r ).sourceDownload();
+  }
+  catch ( const Out::Error & error_r )
+  {
+    return error_r.report( zypper_r );
+  }
+  return zypper_r.exitCode();
+}
diff --git a/src/source-download.h b/src/source-download.h
new file mode 100644 (file)
index 0000000..200418f
--- /dev/null
@@ -0,0 +1,58 @@
+/*---------------------------------------------------------------------------*\
+                          ____  _ _ __ _ __  ___ _ _
+                         |_ / || | '_ \ '_ \/ -_) '_|
+                         /__|\_, | .__/ .__/\___|_|
+                             |__/|_|  |_|
+\*---------------------------------------------------------------------------*/
+
+#ifndef ZYPPER_SOURCE_DOWNLOAD_H
+#define ZYPPER_SOURCE_DOWNLOAD_H
+
+#include "zypp/Pathname.h"
+
+class Zypper;
+
+/*
+      "source-download\n"
+      "\n"
+      "Download source rpms for all installed packages to a local directory.\n"
+      "\n"
+      "  Command options:\n"
+      "-d, --directory <dir>\n"
+      "                     Download all source rpms to this directory.\n"
+      "                     Default: /var/cache/zypper/source-download\n"
+      "--manifest           Write MANIFEST of packages and coresponding source rpms.\n"
+      "--no-manifest        Do not write MANIFEST.\n"
+      "--delete             Delete extraneous source rpms in the local directory.\n"
+      "--no-delete          Do not delete extraneous source rpms.\n"
+      "--dry-run            Don't download any source rpms nor write a MANIFEST,\n"
+      "                     but show which source rpms are missing or extraneous.\n"
+
+      TBD: maybe write manifest file to download directory.
+*/
+
+/** source-download specific options */
+struct SourceDownloadOptions : public Options
+{
+  static const Pathname _defaultDirectory;
+  static const std::string _manifestName;
+
+  SourceDownloadOptions()
+    : _directory( _defaultDirectory )
+//     , _manifest( true )
+    , _delete( true )
+    , _dryrun( false )
+  {}
+
+  Pathname _directory; //< Download all source rpms to this directory.
+//   int _manifest;    //< Whether to write a MANIFEST file.
+  int _delete;         //< Whether to delete extranous source rpms.
+  int _dryrun;         //< Dryrun mode.
+};
+
+/** Download source rpms for all installed packages to a local directory.
+ * \returns zypper.exitCode
+ */
+int sourceDownload( Zypper & zypper_r );
+
+#endif // ZYPPER_SOURCE_DOWNLOAD_H
\ No newline at end of file