@@ -73,7 +73,10 @@ CommandGroup[] getCommands() @safe pure nothrow
7373 new ListOverridesCommand,
7474 new CleanCachesCommand,
7575 new ConvertCommand,
76- )
76+ // This is index management but those commands are hidden
77+ new IndexBuildCommand,
78+ new IndexFromRegistryCommand,
79+ ),
7780 ];
7881}
7982
@@ -273,7 +276,8 @@ unittest {
273276 assert (handler.commandNames == [" init" , " run" , " build" , " test" , " lint" , " generate" ,
274277 " describe" , " clean" , " dustmite" , " fetch" , " add" , " remove" ,
275278 " upgrade" , " add-path" , " remove-path" , " add-local" , " remove-local" , " list" , " search" ,
276- " add-override" , " remove-override" , " list-overrides" , " clean-caches" , " convert" ]);
279+ " add-override" , " remove-override" , " list-overrides" , " clean-caches" , " convert" ,
280+ " index-build" , " index-fromregistry" ]);
277281}
278282
279283// / It sets the cwd as root_path by default
@@ -2983,6 +2987,255 @@ class ConvertCommand : Command {
29832987}
29842988
29852989
2990+ /* *****************************************************************************/
2991+ /* Index management
2992+ /******************************************************************************/
2993+
2994+ public class IndexBuildCommand : Command {
2995+ import dub.index.bitbucket;
2996+ import dub.index.data;
2997+ import dub.index.github;
2998+ import dub.index.gitlab;
2999+ import dub.index.utils;
3000+ import std.random ;
3001+ import std.range ;
3002+
3003+ // / Index file to use
3004+ private string index = " index.yaml" ;
3005+ // / Filename to write to
3006+ private string output = " index-build-result" ;
3007+ // / Packages to filter in - assume all if empty
3008+ private string [] include;
3009+ // / Packages to filter out
3010+ private string [] exclude;
3011+ // / Bearer token to use to authenticate requests
3012+ private string githubToken, gitlabToken, bitbucketToken;
3013+ // / Kind of packages to include (default: all kinds)
3014+ private string [] kind;
3015+ // / Whether to force the iteration of tags or not
3016+ // / This needs to be used if some tags need to be reprocessed
3017+ private bool force_tags;
3018+ // / Force the package to be entirely reprocessed. Imply `--force-tags`.
3019+ private bool force;
3020+ // / Whether to use a randomized sample of packages
3021+ private bool random;
3022+ // / The number of packages to update
3023+ private uint maxUpdates = uint .max;
3024+ // / Source and target index to use, mutually exclusive with `random`
3025+ private uint fromIdx = 0 , toIdx = uint .max;
3026+
3027+ this () @safe pure nothrow
3028+ {
3029+ this .name = " index-build" ;
3030+ this .description = " Generate the rich index from the index.yaml file" ;
3031+ this .helpText = [ " This command is for internal use only. Do not use it." ];
3032+ this .hidden = true ;
3033+ }
3034+
3035+ override void prepare (scope CommandArgs args) {
3036+ args.getopt(" bitbucket-token" , &this .bitbucketToken, [" Bearer token to use when issuing Bitbucket requests" ]);
3037+ args.getopt(" github-token" , &this .githubToken, [" Bearer token to use when issuing Github requests" ]);
3038+ args.getopt(" gitlab-token" , &this .gitlabToken, [" Bearer token to use when issuing GitLab requests" ]);
3039+ args.getopt(" output" , &this .output, [" Where to output the data (path to a folder)" ]);
3040+ args.getopt(" index" , &this .index, [" Index file to use - default to 'index.yaml'" ]);
3041+ args.getopt(" include" , &this .include, [" Which packages to filter in - if not, assume all" ]);
3042+ args.getopt(" exclude" , &this .exclude, [" Which packages to filter out - if not, assume none" ]);
3043+ args.getopt(" kind" , &this .kind, [" Kind of packages to include (github, gitlab, bitbucket). Default: all" ]);
3044+ args.getopt(" force" , &this .force, [" Force Dub to reprocess packages even if it has cache informations" ]);
3045+ args.getopt(" force-tags" , &this .force_tags,
3046+ [" Force Dub to re-list tags, but do not reload the recipe if the commit hasn't changed." ]);
3047+ args.getopt(" random" , &this .random, [" Randomize the order in which packages are processed" ]);
3048+ args.getopt(" max-updates" , &this .maxUpdates, [" Maximum number of packages to process" ]);
3049+ args.getopt(" from" , &this .fromIdx, [" Index to seek to before iterating the list of packages (default: 0)" ]);
3050+ args.getopt(" to" , &this .toIdx, [" Index to stop at when iterating the list of packages (default: end of list)" ]);
3051+
3052+ enforce(this .fromIdx <= this .toIdx, " Cannot have source index (`--from`) be past end index (`--to`)" );
3053+ enforce(this .fromIdx == 0 || ! this .random,
3054+ " Cannot specify source index (`--from`) for random sampling (`--random`)" );
3055+ enforce(this .toIdx == uint .max || ! this .random,
3056+ " Cannot specify end index (`--to`) for random sampling (`--random`)" );
3057+ }
3058+
3059+ override int execute (Dub dub, string [] free_args, string [] app_args)
3060+ {
3061+ import dub.index.client : RepositoryClient;
3062+ import dub.index.data;
3063+ import dub.internal.configy.easy;
3064+ static import std.file ;
3065+ import std.typecons ;
3066+
3067+ enforceUsage(free_args.length == 0 , " Expected no free argument." );
3068+ enforceUsage(app_args.length == 0 , " Expected zero application arguments." );
3069+
3070+ const isUpdate = std.file.exists (this .output);
3071+ if (isUpdate)
3072+ enforce(std.file.isDir (this .output), this .output ~ " : is not a directory" );
3073+ else
3074+ std.file.mkdirRecurse (this .output);
3075+
3076+ auto indexN = parseConfigFileSimple! PackageList(this .index);
3077+ if (indexN.isNull()) return 1 ;
3078+ auto indexC = indexN.get ();
3079+ logInfoNoTag(" Found %s packages in the index file" , indexC.packages.length);
3080+
3081+ const NativePath outputPath = NativePath(this .output);
3082+ size_t processed, updated, notsupported;
3083+ string [] included, excluded, errored;
3084+ scope gh = new GithubClient(this .githubToken);
3085+ scope gl = new GitLabClient(this .gitlabToken);
3086+ scope bb = new BitbucketClient(this .bitbucketToken);
3087+
3088+ void update (scope RepositoryClient client, in PackageEntry pkg) {
3089+ const target = getPackageDescriptionPath(outputPath, PackageName(pkg.name.value));
3090+ ensureDirectory(target.parentPath());
3091+ const targetStr = target.toNativeString();
3092+ auto previous = ! this .force && std.file.exists (targetStr) ?
3093+ parseConfigFileSimple! (IndexedPackage! 0 )(targetStr, StrictMode.Ignore) :
3094+ Nullable! (IndexedPackage! 0 ).init;
3095+ if (this .force_tags && ! previous.isNull())
3096+ previous.get ().cache = CacheInfo.init;
3097+ auto res = updateDescription(client, pkg, previous);
3098+ if (previous.isNull() || previous.get () != res) {
3099+ std.file.write (targetStr, res.serializeToJsonString());
3100+ ++ updated;
3101+ }
3102+ }
3103+
3104+ // Update a single package - this code is in its own function as it
3105+ // is wrapped in a try-catch in the `foreach` to process as many packages
3106+ // as possible
3107+ void updatePackageIndex (in PackageEntry pkg) {
3108+ logInfo(" [%s] Processing included package" , pkg.name.value);
3109+ switch (pkg.source.kind) {
3110+ case ` github` :
3111+ scope client = gh.new Repository(pkg.source.owner, pkg.source.project);
3112+ update(client, pkg);
3113+ break ;
3114+ case ` gitlab` :
3115+ scope client = gl.new Project(pkg.source.owner, pkg.source.project);
3116+ update(client, pkg);
3117+ break ;
3118+ case ` bitbucket` :
3119+ scope client = bb.new Repository(pkg.source.owner, pkg.source.project);
3120+ update(client, pkg);
3121+ break ;
3122+ default :
3123+ throw new Exception (" Package kind not supported: " ~ pkg.source.kind);
3124+ }
3125+ }
3126+
3127+ int processEntry (size_t idx, ref PackageEntry pkg) {
3128+ if (updated >= this .maxUpdates) return 1 ;
3129+ if (this .include.length && ! this .include.canFind(pkg.name.value))
3130+ return 0 ;
3131+ if (this .exclude.canFind(pkg.name.value)) {
3132+ excluded ~= pkg.name.value;
3133+ return 0 ;
3134+ }
3135+ if (this .kind.length && ! this .kind.canFind(pkg.source.kind))
3136+ return 0 ;
3137+
3138+ ++ processed;
3139+ try
3140+ updatePackageIndex(pkg);
3141+ catch (Exception exc) {
3142+ errored ~= pkg.name.value;
3143+ // If we get a 404 here, it might be a dead package
3144+ logError(" [%s] Could not build index for package: %s" ,
3145+ pkg.name.value, exc.message());
3146+ }
3147+
3148+ if (this .include.length) {
3149+ included ~= pkg.name.value;
3150+ if (included.length % 10 == 0 ) {
3151+ const rl = gh.getRateLimit();
3152+ logDebug(" Requests still available: %s/%s" , rl.remaining, rl.limit);
3153+ }
3154+ }
3155+ else if (idx % 10 == 0 ) {
3156+ const rl = gh.getRateLimit();
3157+ logDebug(" Requests still available: %s/%s" , rl.remaining, rl.limit);
3158+ }
3159+ return 0 ;
3160+ }
3161+
3162+ if (this .random) { // Can't use `std.random : choose` because bugs
3163+ foreach (idx, pkg; indexC.packages.randomCover().enumerate)
3164+ if (processEntry(idx, pkg))
3165+ break ;
3166+ } else {
3167+ const startIdx = min(this .fromIdx, indexC.packages.length);
3168+ const endIdx = min(this .toIdx, indexC.packages.length);
3169+ foreach (idx, pkg; indexC.packages[startIdx .. endIdx])
3170+ if (processEntry(idx, pkg))
3171+ break ;
3172+ }
3173+
3174+ logInfoNoTag(" Updated %s packages out of %s processed (%s excluded, %s errors, %s not supported)" ,
3175+ updated, processed, excluded.length, errored.length, notsupported);
3176+ if (this .include.length && included != this .include)
3177+ logWarn(" Not all explicitly-included packages have been processed!" );
3178+ if (errored.length)
3179+ logWarn(" The following packages errored out:\n %(\t - %s\n %)" , errored);
3180+ if (! this .kind.length || this .kind.canFind(` github` )) {
3181+ const rl = gh.getRateLimit();
3182+ logInfoNoTag(" Github requests still available: %s/%s" , rl.remaining, rl.limit);
3183+ }
3184+ return 0 ;
3185+ }
3186+ }
3187+
3188+ public class IndexFromRegistryCommand : Command {
3189+ // / Filename to write to
3190+ private string output = " index.yaml" ;
3191+ // / Bypass cache, always query the registry
3192+ private bool force;
3193+
3194+ this () @safe pure nothrow
3195+ {
3196+ this .name = " index-fromregistry" ;
3197+ this .description = " Generate the index.yaml file from the remote registry" ;
3198+ this .helpText = [ " This command is for internal use only. Do not use it." ];
3199+ this .hidden = true ;
3200+ }
3201+
3202+ override void prepare (scope CommandArgs args) {
3203+ args.getopt(" O" , &this .output, [" Where to output the data ('-' is supported)" ]);
3204+ args.getopt(" f" , &this .force, [" Bypass the cache and always query the registry" ]);
3205+ }
3206+
3207+ override int execute (Dub dub, string [] free_args, string [] app_args)
3208+ {
3209+ import dub.internal.vibecompat.inet.url;
3210+ import dub.packagesuppliers.registry;
3211+ import std.format ;
3212+
3213+ enforceUsage(free_args.length == 0 , " Expected zero arguments." );
3214+ enforceUsage(app_args.length == 0 , " Expected zero application arguments." );
3215+
3216+ scope registry = new RegistryPackageSupplier(URL (defaultRegistryURLs[1 ]));
3217+ scope allPkgs = registry.getPackageDump(this .force);
3218+ writeln(" Found " , allPkgs.array.length, " packages" );
3219+ scope output = this .output == " -" ? stdout : File (this .output, " w+" );
3220+ scope writer = output.lockingTextWriter();
3221+ writer.formattedWrite(" packages:\n " );
3222+ foreach (pkg; allPkgs.array) {
3223+ writer.formattedWrite(` %s:
3224+ source:
3225+ kind: %s
3226+ owner: %s
3227+ project: %s
3228+ ` ,
3229+ pkg[" name" ].opt! string , pkg[" repository" ][" kind" ].opt! string ,
3230+ pkg[" repository" ][" owner" ].opt! string ,
3231+ pkg[" repository" ][" project" ].opt! string );
3232+ }
3233+
3234+ return 0 ;
3235+ }
3236+ }
3237+
3238+
29863239/* *****************************************************************************/
29873240/* HELP */
29883241/* *****************************************************************************/
0 commit comments