├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── clisource ├── app.d └── options.d ├── dub.json ├── source └── dubproxy │ ├── git.d │ ├── gittest.d │ ├── options.d │ ├── package.d │ └── parsetest.d └── testproxyfile.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | indent_size = 4 7 | tab_width = 4 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = tr 12 | max_line_length = 80 13 | 14 | [*.yaml] 15 | indent_style = space 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dub 2 | docs.json 3 | __dummy.html 4 | docs/ 5 | /dubproxy 6 | dubproxy.so 7 | dubproxy.dylib 8 | dubproxy.dll 9 | dubproxy.a 10 | dubproxy.lib 11 | dubproxy-test-* 12 | *.exe 13 | *.o 14 | *.obj 15 | *.lst 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: d 2 | sudo: false 3 | 4 | d: 5 | - dmd 6 | - ldc 7 | 8 | script: 9 | - dub test --compiler=${DC} 10 | 11 | notifications: 12 | email: false 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dubproxy 2 | 3 | [![Build Status](https://travis-ci.org/symmetryinvestments/dubproxy.svg?branch=master)](https://travis-ci.org/symmetryinvestments/dubproxy) 4 | dubproxy is a library and cli to allow use of private dub packages and mirror 5 | code.dlang.org, without a private registry. 6 | It is a standalone library/cli and is completely transparent for dub. 7 | 8 | ## private libraries 9 | 10 | Sometimes a dub project needs access to a private library. 11 | Subpackages are one solution, but getting dub to correctly work with subpackages 12 | is not always easy. 13 | Therefor, it is sometimes desirable to complete split out subpackages into there 14 | own dub project. 15 | Dubproxy allows to do that. 16 | One of dubproxy's features is to take local/remote dub projects, located in a 17 | git, and insert them into ~/.dub/packages such that dub thinks its just another 18 | package from code.dlang.org. 19 | 20 | ## code.dlang.org mirroring 21 | 22 | Code.dlang.org is not always accessible, but a still might be required right 23 | now. 24 | Maybe you are on a flight to dconf or code.dlang.org is down. 25 | Dubproxy allows you to get a storable list of all packages and upstream urls. 26 | This list can then be used by dubproxy to get a particular package or 27 | package-version. 28 | You need internet access of course. 29 | As time of writing this Aug. 2019 all gits of all package of code.dlang.org 30 | require about 6.5 GB of space. 31 | 32 | ## Examples 33 | 34 | 1. Get dubproxy(cli) 35 | ```sh 36 | $ dub fetch dubproxy 37 | $ dub build --config=cli 38 | ``` 39 | 40 | 2. put dubproxy in your path or use `$ dub run dubproxy --` 41 | 3. get list of code.dlang.org packages 42 | ```sh 43 | $ dubproxy -m -n codedlangorg.json 44 | ``` 45 | 46 | 4. get a package from a dubproxyfile 47 | ```sh 48 | $ dubproxy -i codedlangorg.json -g xlsxd 49 | ``` 50 | By default dubproxy will try to place the git in system default dub directory. 51 | 52 | 5. get a package and place the git a user specified directory 53 | ```sh 54 | $ dubproxy -i codeldangorg.json -g xlsxreader -f GitCloneFolder 55 | $ dubproxy -i codeldangorg.json -g xlsxreader:v0.6.1 -f GitCloneFolder 56 | ``` 57 | 58 | 6. place the dub package in a user specified directory 59 | ```sh 60 | $ dubproxy -i codeldangorg.json -g graphqld -o SomePackageFolder 61 | ``` 62 | 63 | 7. get multiple packages 64 | ```sh 65 | $ dubproxy -i codeldangorg.json -g graphqld -g inifiled 66 | ``` 67 | 68 | 8. get all packages in a file (run before long flight) 69 | ```sh 70 | $ dubproxy -i codeldangorg.json -a -u 71 | ``` 72 | 73 | The `-u` is necessary to disable user interaction, because some listed packages 74 | on code.dlang.org do not exist anymore and github.com therefore askeds for a 75 | username password combination. 76 | 77 | 9. dub is not in your path 78 | ```sh 79 | $ dubproxy -d path_to_dub 80 | ``` 81 | 82 | 10. git is not in your path 83 | ```sh 84 | $ dubproxy -p path_to_git 85 | ``` 86 | 87 | 11. generate a dummy dubproxy.json file with filename myPrivateProjects.json 88 | ```sh 89 | $ dubproxy --dummy --dummyPath myPrivateProjects.json 90 | ``` 91 | 92 | 12. get help 93 | ```sh 94 | $ dubproxy -h 95 | ``` 96 | -------------------------------------------------------------------------------- /clisource/app.d: -------------------------------------------------------------------------------- 1 | import std.array : empty; 2 | import std.algorithm.searching : startsWith; 3 | import std.stdio; 4 | import std.getopt; 5 | import std.string : indexOf; 6 | import std.experimental.logger; 7 | import std.file : exists; 8 | import std.path : buildPath; 9 | 10 | 11 | import dubproxy; 12 | import dubproxy.git; 13 | 14 | import opts = options; 15 | 16 | int main(string[] args) { 17 | auto helpInformation = opts.parseOptions(args); 18 | 19 | if(helpInformation.helpWanted) { 20 | defaultGetoptPrinter("Dubproxy is a tool to make dub work with " 21 | ~ "private repos and to bypass code.dlang.org to fetch packages", 22 | helpInformation.options); 23 | return 0; 24 | } 25 | 26 | if(opts.options().verbose) { 27 | globalLogLevel(LogLevel.trace); 28 | } else { 29 | globalLogLevel(LogLevel.error); 30 | } 31 | 32 | if(opts.options.dummyDubProxy) { 33 | tracef("dummyDubProxy %s", opts.options.dummyDubProxyPath); 34 | DubProxyFile dpf; 35 | dpf.insertPath("dummy", "https://does_not_exist.git"); 36 | toFile(dpf, opts.options.dummyDubProxyPath ~ "/dubproxy.json"); 37 | } 38 | 39 | if(opts.options.mirrorCodeDlang) { 40 | tracef("mirrorCodeDlang %s", opts.options.mirrorFilename); 41 | DubProxyFile dpf = getCodeDlangOrgCopy(); 42 | toFile(dpf, opts.options.mirrorFilename); 43 | } 44 | 45 | if(!opts.options.showTagsPath.empty) { 46 | tracef("showTags %s", opts.options.proxyFile); 47 | TagReturn[] tags; 48 | if(exists(opts.options.proxyFile)) { 49 | DubProxyFile dpf = fromFile(opts.options.proxyFile); 50 | if(dpf.pkgExists(opts.options.showTagsPath)) { 51 | tags = getTags(dpf.getPath(opts.options.showTagsPath), 52 | opts.options.tagKind, opts.options.libOptions); 53 | } 54 | } else if(exists(opts.options.showTagsPath)) { 55 | tags = getTags(opts.options.showTagsPath, opts.options.tagKind, 56 | opts.options.libOptions); 57 | } 58 | 59 | if(tags.empty) { 60 | writefln!"Could not find and tags for path '%s'" 61 | (opts.options.showTagsPath); 62 | return 1; 63 | } 64 | 65 | foreach(it; tags) { 66 | writefln("%s %s", it.hash, it.tag); 67 | } 68 | return 0; 69 | } 70 | 71 | if(opts.options.cloneAll || opts.options.cloneAllNoTerminal) { 72 | tracef("cloneAll proxyfile %s", opts.options.proxyFile); 73 | bool worked = true; 74 | DubProxyFile dpf = fromFile(opts.options.proxyFile); 75 | const len = dpf.packages.length; 76 | size_t i = 1; 77 | foreach(key, value; dpf.packages) { 78 | writefln!"Getting git for '%s' %d of %d"(key, i, len); 79 | try { 80 | getPackage(dpf, key); 81 | } catch(Exception e) { 82 | worked = false; 83 | writefln!"Update to get '%s' with msg '%s'"(key, e.toString()); 84 | } 85 | ++i; 86 | } 87 | } 88 | 89 | if(!opts.options.proxyFile.empty && opts.options.genAllTags) { 90 | tracef("genTags proxyFile %s, gitFolder %s", opts.options.proxyFile, 91 | opts.options.gitFolder); 92 | DubProxyFile dpf = fromFile(opts.options.proxyFile); 93 | tracef("_\tpackages %s", dpf.packages); 94 | foreach(key, value; dpf.packages) { 95 | try { 96 | tracef("_\t_\tbuild tag it %s", key); 97 | const gitDestDir = buildPath(opts.options.packageFolder, key); 98 | tracef("_\t_\tgitDestDir %s", gitDestDir); 99 | const GetSplit s = splitLocal(gitDestDir); 100 | tracef("_\t_\tsplit %s", s); 101 | TagReturn[] allTags = getTags(gitDestDir, TagKind.all, 102 | opts.options.libOptions); 103 | tracef("_\t_\tallTags %s", allTags); 104 | foreach(tag; allTags) { 105 | tracef("_\t_\t_\tbuild tag %s", tag); 106 | const ver = tag.getVersion(); 107 | if(s.ver.empty || s.ver == ver) { 108 | tracef("_\t_\t_\tactually build tag split %s destdir %s" 109 | ~ " packageFolder %s", ver, gitDestDir, 110 | opts.options.packageFolder); 111 | try { 112 | createWorkingTree(gitDestDir, tag, s.pkg, 113 | opts.options.packageFolder, 114 | opts.options.libOptions); 115 | } catch(Exception e) { 116 | errorf("Failed to create working tree %s", 117 | e.toString()); 118 | } 119 | } 120 | } 121 | } catch(Exception e) { 122 | error(e.toString()); 123 | } 124 | } 125 | } 126 | 127 | if(!opts.options.proxyFile.empty && !opts.options.packages.empty) { 128 | tracef("get %s", opts.options.packages); 129 | DubProxyFile dpf = fromFile(opts.options.proxyFile); 130 | foreach(pkg; opts.options.packages) { 131 | GetSplit sp = pkg.indexOf(":") != -1 132 | ? splitGet(pkg) 133 | : splitLocal(pkg); 134 | sp.ver = !sp.ver.empty && !startsWith(sp.ver, "v") 135 | ? "v" ~ sp.ver 136 | : sp.ver; 137 | 138 | string gitDestDir = getPackage(dpf, sp.pkg); 139 | tracef("path to cloned git '%s'", gitDestDir); 140 | const GetSplit s = splitLocal(gitDestDir); 141 | TagReturn[] allTags = getTags(gitDestDir, TagKind.all, 142 | opts.options.libOptions); 143 | tracef("_\t_\tallTags %s", allTags); 144 | foreach(tag; allTags) { 145 | tracef("_\t_\t_\tbuild tag %s", tag); 146 | const ver = tag.getVersion(); 147 | tracef("_\t_\t_\tsp.ver %s == ver %s", sp.ver, ver); 148 | if(sp.ver == ver) { 149 | tracef("_\t_\t_\tactually build tag split %s destdir %s" 150 | ~ " packageFolder %s", ver, gitDestDir, 151 | opts.options.packageFolder); 152 | try { 153 | createWorkingTree(gitDestDir, tag, s.pkg, 154 | opts.options.packageFolder, 155 | opts.options.libOptions); 156 | } catch(Exception e) { 157 | errorf("Failed to create working tree %s", 158 | e.toString()); 159 | } 160 | } 161 | } 162 | } 163 | } 164 | 165 | return 0; 166 | } 167 | 168 | string getPackage(ref const(DubProxyFile) dpf, string pkg) { 169 | if(!dpf.pkgExists(pkg)) { 170 | writefln!"No package '%s' exists in DubProxyFile '%s'"(pkg, 171 | opts.options.proxyFile); 172 | } 173 | 174 | const string pkgPath = dpf.getPath(pkg); 175 | const PathKind pk = getPathKind(pkgPath); 176 | const gitDestDir = buildPath(opts.options.gitFolder, pkg); 177 | final switch(pk) { 178 | case PathKind.remoteGit: 179 | cloneBare(pkgPath, LocalGit.no, gitDestDir, 180 | opts.options.libOptions); 181 | break; 182 | case PathKind.localGit: 183 | cloneBare(pkgPath, LocalGit.yes, gitDestDir, 184 | opts.options.libOptions); 185 | break; 186 | case PathKind.folder: 187 | assert(false, "TODO"); 188 | } 189 | return gitDestDir; 190 | } 191 | 192 | struct GetSplit { 193 | string pkg; 194 | string ver; 195 | } 196 | 197 | GetSplit splitGet(string str) { 198 | import std.string : indexOf; 199 | 200 | GetSplit ret; 201 | const colon = str.indexOf(':'); 202 | if(colon == -1) { 203 | ret.pkg = str; 204 | } else { 205 | ret.pkg = str[0 .. colon]; 206 | ret.ver = str[colon + 1 .. $]; 207 | } 208 | return ret; 209 | } 210 | 211 | GetSplit splitLocal(string str) { 212 | import std.string : lastIndexOf; 213 | 214 | GetSplit ret; 215 | const colon = str.lastIndexOf('/'); 216 | if(colon == -1) { 217 | ret.pkg = str; 218 | } else { 219 | ret.pkg = str[colon + 1 .. $]; 220 | } 221 | return ret; 222 | } 223 | -------------------------------------------------------------------------------- /clisource/options.d: -------------------------------------------------------------------------------- 1 | module options; 2 | 3 | import std.getopt; 4 | import dubproxy.options : DubProxyOptions; 5 | 6 | @safe: 7 | 8 | struct DubProxyCliOptions { 9 | import dubproxy.git : TagKind; 10 | DubProxyOptions libOptions; 11 | bool verbose; 12 | 13 | bool mirrorCodeDlang; 14 | string mirrorFilename = "code.json"; 15 | string[] packages; 16 | string proxyFile = "dubproxy.json"; 17 | string packageFolder = getDefaultPackageFolder(); 18 | string gitFolder = getDefaultPackageFolder(); 19 | 20 | bool dummyDubProxy; 21 | string dummyDubProxyPath = "."; 22 | 23 | string showTagsPath; 24 | TagKind tagKind = TagKind.all; 25 | 26 | bool cloneAll; 27 | bool cloneAllNoTerminal; 28 | bool genAllTags; 29 | } 30 | 31 | private DubProxyCliOptions __options; 32 | 33 | ref DubProxyCliOptions writeAbleOptions() { 34 | return __options; 35 | } 36 | 37 | @property ref const(DubProxyCliOptions) options() { 38 | return __options; 39 | } 40 | 41 | string getDefaultPackageFolder() pure { 42 | version(Posix) { 43 | return "~/.dub/packages/"; 44 | } else version(Windows) { 45 | return `%APPDATA%\dub\packages\`; 46 | } else { 47 | static assert(false, "Unsupported platform"); 48 | } 49 | } 50 | 51 | string getDefaultGitFolder() pure { 52 | version(Posix) { 53 | return "~/.dub/DubProxyGits/"; 54 | } else version(Windows) { 55 | return `%APPDATA%\dub\DubProxyGits\`; 56 | } else { 57 | static assert(false, "Unsupported platform"); 58 | } 59 | } 60 | 61 | 62 | GetoptResult parseOptions(ref string[] args) { 63 | auto helpInformation = getopt(args, 64 | "m|mirror", 65 | "Get a list of packages currently available on code.dlang.org" 66 | ~ "\n\t\t\tand store a file specified in \"n|mirrorFileName\"", 67 | &writeAbleOptions().mirrorCodeDlang, 68 | 69 | "n|mirrorFileName", 70 | "The filename where to store the packages available on code.dlang.org", 71 | &writeAbleOptions().mirrorFilename, 72 | 73 | "d|dubPath", 74 | "The path to the dub executable", 75 | &writeAbleOptions().libOptions.pathToDub, 76 | 77 | "p|gitPath", 78 | "The path to the git executable", 79 | &writeAbleOptions().libOptions.pathToGit, 80 | 81 | "f|gitFolder", 82 | "The path where the gits get cloned to", 83 | &writeAbleOptions().gitFolder, 84 | 85 | "overrideGit", 86 | "Allow to override the git folder of cloned packages", 87 | &writeAbleOptions().libOptions.ovrGF, 88 | 89 | "overrideTree", 90 | "Allow to override the git worktree folder of cloned package version", 91 | &writeAbleOptions().libOptions.ovrWTF, 92 | 93 | "g|get", 94 | "Get a precific package. \"-g dub\" will fetch dub and create" 95 | ~ "\n\t\t\tfolders for all version tags for dub. \"-g dub:1.1.0\" will " 96 | ~ "\n\t\t\ttry to get dub and create a package for v1.1.0. " 97 | ~ "\n\t\t\t\"g dub:~master\" will try get dub and create a package for " 98 | ~ "\n\t\t\t~master", 99 | &writeAbleOptions().packages, 100 | 101 | "i|proxyFile", 102 | "The filename of the dubproxy file to search packages in", 103 | &writeAbleOptions().proxyFile, 104 | 105 | "o|packagesFolder", 106 | "The path where packages should be stored", 107 | &writeAbleOptions().packageFolder, 108 | 109 | "dummy", 110 | "Generate a empty dubproxy.json file", 111 | &writeAbleOptions().dummyDubProxy, 112 | 113 | "dummyPath", 114 | "Path to the folder where to create the dummy dubproxy.json file", 115 | &writeAbleOptions().dummyDubProxyPath, 116 | 117 | "t|tags", 118 | "Show tags for passed dirpath or url", 119 | &writeAbleOptions().showTagsPath, 120 | 121 | "k|tagsKind", 122 | "Limit tags to a specific kind of tags", 123 | &writeAbleOptions().tagKind, 124 | 125 | "a|cloneAll", 126 | "Clone or fetch all packages provided in \"i|input\"", 127 | &writeAbleOptions().cloneAll, 128 | 129 | "u|noUserInteraction", 130 | "Run git without user interaction", 131 | &writeAbleOptions().libOptions.noUserInteraction, 132 | 133 | "v|verbose", 134 | "Get some more output of what is going on", 135 | &writeAbleOptions().verbose, 136 | 137 | "genAllTags", 138 | "Generate tags for all the repos in gitFolder listed in proxyFile", 139 | &writeAbleOptions().genAllTags, 140 | ); 141 | 142 | return helpInformation; 143 | } 144 | -------------------------------------------------------------------------------- /dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "authors": [ 3 | "Robert burner Schadek" 4 | ], 5 | "copyright": "Copyright © 2019, Symmetry Investments", 6 | "type": "library", 7 | "dependencies": { 8 | "urld": "~>2.1.1" 9 | }, 10 | "configurations": [ 11 | { 12 | "name": "cli", 13 | "targetType": "executable", 14 | "sourcePaths": ["source/", "clisource/"], 15 | "importPaths": ["source/", "clisource/"] 16 | }, 17 | { 18 | "name": "dubproxy", 19 | "targetType": "library", 20 | "sourcePaths": ["source/"], 21 | "importPaths": ["source/"] 22 | } 23 | ], 24 | "description": "A proxy for dub to allow local dependency resolution", 25 | "license": "LGPL3", 26 | "name": "dubproxy" 27 | } 28 | -------------------------------------------------------------------------------- /source/dubproxy/git.d: -------------------------------------------------------------------------------- 1 | module dubproxy.git; 2 | 3 | import std.array : empty; 4 | import std.algorithm.iteration : filter, splitter; 5 | import std.algorithm.searching : startsWith; 6 | import std.exception : enforce; 7 | import std.experimental.logger; 8 | import std.file : exists, isDir, mkdirRecurse, rmdirRecurse, getcwd, chdir, 9 | readText; 10 | import std.path : absolutePath, expandTilde, asNormalizedPath, 11 | buildNormalizedPath; 12 | import std.stdio : File; 13 | import std.format : format; 14 | import std.process : executeShell; 15 | import std.typecons : Flag; 16 | import std.string : split, strip; 17 | 18 | import url; 19 | 20 | import dubproxy.options; 21 | 22 | @safe: 23 | 24 | struct TagReturn { 25 | string hash; 26 | string tag; 27 | 28 | string getVersion() const { 29 | import std.string : lastIndexOf; 30 | 31 | enforce(!this.tag.empty, "Can not compute version of empty tag"); 32 | const idx = this.tag.lastIndexOf('/'); 33 | if(idx == -1) { 34 | return this.tag; 35 | } 36 | return this.tag[idx + 1 .. $]; 37 | } 38 | } 39 | 40 | string getHashFromVersion(const(TagReturn[]) tags, string ver) pure { 41 | import std.algorithm.searching : endsWith; 42 | foreach(it; tags) { 43 | if(it.tag.endsWith(ver)) { 44 | return it.hash; 45 | } 46 | } 47 | return ""; 48 | } 49 | 50 | enum TagKind { 51 | branch, 52 | pull, 53 | tags, 54 | all 55 | } 56 | 57 | TagReturn[] getTags(string path, const(TagKind) tk, 58 | ref const(DubProxyOptions) options) 59 | { 60 | URL u; 61 | if(exists(path) && isDir(path)) { 62 | return getTagsLocal(path, tk, options); 63 | } else if(tryParseURL(path, u)) { 64 | return getTagsRemote(path, tk, options); 65 | } else { 66 | assert(false, format!"Path '%s' could be resolved to get git tags" 67 | (path)); 68 | } 69 | } 70 | 71 | string getTagDataLocal(const string path, const(TagKind) tk, 72 | ref const(DubProxyOptions) options) 73 | { 74 | auto oldCwd = getcwd(); 75 | chdir(path); 76 | scope(exit) { 77 | chdir(oldCwd); 78 | } 79 | 80 | const toExe = tk == TagKind.tags 81 | ? format!`%s%s ls-remote --tags --sort="-version:refname" .`( 82 | options.noUserInteraction ? "GIT_TERMINAL_PROMPT=0 " : "", 83 | options.pathToGit) 84 | : format!`%s%s ls-remote .`( 85 | options.noUserInteraction ? "GIT_TERMINAL_PROMPT=0 " : "", 86 | options.pathToGit); 87 | 88 | auto rslt = executeShell(toExe); 89 | enforce(rslt.status == 0, format! 90 | "'%s' returned with '%d' 0 was expected output '%s'"( 91 | toExe, rslt.status, rslt.output)); 92 | return rslt.output; 93 | } 94 | 95 | TagReturn[] getTagsLocal(string path, const(TagKind) tk, 96 | ref const(DubProxyOptions) options) 97 | { 98 | string data = getTagDataLocal(path, tk, options); 99 | return processTagData(data, tk); 100 | } 101 | 102 | string getTagDataRemote(string path, const(TagKind) tk, 103 | ref const(DubProxyOptions) options) 104 | { 105 | const toExe = tk == TagKind.tags 106 | ? format!`%s%s ls-remote --tags --sort="-version:refname" %s`( 107 | options.noUserInteraction ? "GIT_TERMINAL_PROMPT=0 " : "", 108 | options.pathToGit, path 109 | ) 110 | : format!`%s%s ls-remote %s`( 111 | options.noUserInteraction ? "GIT_TERMINAL_PROMPT=0 " : "", 112 | options.pathToGit, path); 113 | 114 | auto rslt = executeShell(toExe); 115 | enforce(rslt.status == 0, format! 116 | "'%s' returned with '%d' 0 was expected output '%s' cwd '%s'"( 117 | toExe, rslt.status, rslt.output, getcwd())); 118 | return rslt.output; 119 | } 120 | 121 | TagReturn[] getTagsRemote(string path, const(TagKind) tk, 122 | ref const(DubProxyOptions) options) 123 | { 124 | string data = getTagDataRemote(path, tk, options); 125 | return processTagData(data, tk); 126 | } 127 | 128 | private TagReturn[] processTagData(string data, const(TagKind) tk) { 129 | import std.algorithm.searching : canFind, count; 130 | 131 | const kindFilter = tk == TagKind.branch ? "heads" 132 | : tk == TagKind.pull ? "pull" 133 | : tk == TagKind.tags ? "tags" : ""; 134 | 135 | TagReturn[] ret; 136 | foreach(line; data.splitter("\n") 137 | .filter!(line => !line.empty) 138 | .filter!(line => !canFind(line, "^{}")) 139 | .filter!(line => count(line, "/") == 2) 140 | .filter!(line => kindFilter.empty || line.canFind(kindFilter))) 141 | { 142 | string[] lineSplit = line.split('\t'); 143 | enforce(lineSplit.length == 2, format! 144 | "Line '%s' split incorrectly in '%s'"(line, lineSplit)); 145 | 146 | ret ~= TagReturn(lineSplit[0].strip(" \t\n\r"), 147 | lineSplit[1].strip(" \t\n\r")); 148 | } 149 | return ret; 150 | } 151 | 152 | alias LocalGit = Flag!"LocalGit"; 153 | 154 | void cloneBare(string path, const LocalGit lg, string destDir, 155 | ref const(DubProxyOptions) options) 156 | { 157 | void clone() { 158 | const toExe = format!`%s%s clone --bare%s %s %s`( 159 | options.noUserInteraction ? "GIT_TERMINAL_PROMPT=0 " : "", 160 | options.pathToGit, 161 | lg == LocalGit.yes ? " -l" : "", path, destDir); 162 | auto rslt = executeShell(toExe); 163 | enforce(rslt.status == 0, format! 164 | "'%s' returned with '%d' 0 was expected output '%s'"( 165 | toExe, rslt.status, rslt.output)); 166 | } 167 | 168 | const string absDestDir = destDir.expandTilde() 169 | .absolutePath() 170 | .buildNormalizedPath(); 171 | 172 | const bool e = exists(absDestDir); 173 | 174 | if(e && options.ovrGF == OverrideGitFolder.yes) { 175 | () @trusted { rmdirRecurse(absDestDir); }(); 176 | clone(); 177 | } else if(!e) { 178 | clone(); 179 | } else { 180 | auto oldCwd = getcwd(); 181 | chdir(absDestDir); 182 | scope(exit) { 183 | chdir(oldCwd); 184 | } 185 | const toExe = format!`%s%s fetch --all`( 186 | options.noUserInteraction ? "GIT_TERMINAL_PROMPT=0 " : "", 187 | options.pathToGit); 188 | auto rslt = executeShell(toExe); 189 | enforce(rslt.status == 0, format! 190 | "'%s' returned with '%d' 0 was expected output '%s'"( 191 | toExe, rslt.status, rslt.output)); 192 | } 193 | } 194 | 195 | void createWorkingTree(string clonedGitPath, const(TagReturn) tag, 196 | string packageName, string destDir, ref const(DubProxyOptions) options) 197 | { 198 | const ver = tag.getVersion(); 199 | const verTag = ver.startsWith("v") ? ver[1 .. $] : ver; 200 | const absGitPath = buildNormalizedPath(absolutePath(expandTilde(clonedGitPath))); 201 | const absDestDir = buildNormalizedPath(absolutePath(expandTilde(destDir))); 202 | const rsltPath = format!"%s/%s-%s/%s"(absDestDir, packageName, verTag, 203 | packageName); 204 | tracef("rsltPath %s, absGitPath %s, absDestDir %s, packageName %s, verTag %s", 205 | rsltPath, absGitPath, absDestDir, packageName, verTag); 206 | 207 | const bool e = exists(rsltPath); 208 | enforce(!e || options.ovrWTF == OverrideWorkTreeFolder.yes, format!( 209 | "Path '%s' exist and override flag was not passed")(rsltPath)); 210 | 211 | if(e) { 212 | () @trusted { rmdirRecurse(rsltPath); }(); 213 | } else { 214 | mkdirRecurse(rsltPath); 215 | } 216 | 217 | const string cwd = getcwd(); 218 | scope(exit) { 219 | chdir(cwd); 220 | enforce(getcwd() == cwd, format! 221 | "Failed to change paths to '%s' cwd '%s'"(cwd, getcwd())); 222 | } 223 | 224 | tracef("chdir '%s'", absGitPath); 225 | chdir(absGitPath); 226 | enforce(getcwd() == absGitPath, 227 | format!"Failed change to paths to '%s'"(absGitPath)); 228 | 229 | const toExe = format!"%s worktree add -f %s %s"(options.pathToGit, rsltPath, 230 | ver); 231 | tracef("toExe '%s'", toExe); 232 | auto rslt = executeShell(toExe); 233 | enforce(rslt.status == 0, format! 234 | "'%s' returned with '%d' 0 was expected output '%s'"( 235 | toExe, rslt.status, rslt.output)); 236 | 237 | insertVersionIntoDubFile(rsltPath, ver, options); 238 | } 239 | 240 | void insertVersionIntoDubFile(string packageDir, string ver, 241 | ref const(DubProxyOptions) options) 242 | { 243 | const js = format!"%s/dub.json"(packageDir); 244 | const jsE = exists(js); 245 | const pkg = format!"%s/package.json"(packageDir); 246 | const pkgE = exists(pkg); 247 | const sdl = format!"%s/dub.sdl"(packageDir); 248 | const sdlE = exists(sdl); 249 | const cVer = ver.startsWith("v") 250 | ? ver[1 .. $] 251 | : ver.startsWith("~") ? ver : "~" ~ ver; 252 | 253 | if(jsE) { 254 | insertVersionIntoDubJsonFile(js, cVer); 255 | } else if(sdlE) { 256 | insertVersionIntoDubSDLFile(sdl, cVer, options); 257 | } else if(pkgE) { 258 | insertVersionIntoDubJsonFile(pkg, cVer); 259 | } else { 260 | enforce(false, format!"could not find a dub.{json,sdl} file in '%s'" 261 | (packageDir)); 262 | } 263 | } 264 | 265 | void insertVersionIntoDubJsonFile(string fileName, string ver) { 266 | import std.json : JSONValue, parseJSON; 267 | fileName = buildNormalizedPath(absolutePath(expandTilde(fileName))); 268 | JSONValue j = parseJSON(readText(fileName)); 269 | j["version"] = ver; 270 | 271 | auto f = File(fileName, "w"); 272 | f.write(j.toPrettyString()); 273 | f.writeln(); 274 | } 275 | 276 | void insertVersionIntoDubSDLFile(string fileName, string ver, 277 | ref const(DubProxyOptions) options) 278 | { 279 | import std.path : dirName; 280 | const pth = dirName(buildNormalizedPath(absolutePath(expandTilde(fileName)))); 281 | const string cwd = getcwd(); 282 | scope(exit) { 283 | chdir(cwd); 284 | } 285 | chdir(pth); 286 | const toExe = format!`%s convert --format=json`(options.pathToDub); 287 | 288 | auto rslt = executeShell(toExe); 289 | enforce(rslt.status == 0, format!( 290 | "dub failed with code '%s' and output '%s' to convert dub.sdl to " 291 | ~ "dub.json with cmd '%s'")(rslt.status, rslt.output, toExe)); 292 | 293 | const jsFn = pth ~ "/dub.json"; 294 | insertVersionIntoDubJsonFile(jsFn, ver); 295 | } 296 | 297 | enum PathKind { 298 | remoteGit, 299 | localGit, 300 | folder 301 | } 302 | 303 | PathKind getPathKind(string path) { 304 | if(exists(path) && isDir(path) && exists(path ~ "/.git")) { 305 | return PathKind.localGit; 306 | } else if(exists(path) && isDir(path) && !exists(path ~ "/.git")) { 307 | return PathKind.folder; 308 | } 309 | 310 | URL u; 311 | if(tryParseURL(path, u)) { 312 | return PathKind.remoteGit; 313 | } 314 | 315 | throw new Exception(format!"Couldn't determine PathKind for '%s'"(path)); 316 | } 317 | -------------------------------------------------------------------------------- /source/dubproxy/gittest.d: -------------------------------------------------------------------------------- 1 | module dubproxy.gittest; 2 | 3 | import std.stdio; 4 | import std.format : format; 5 | 6 | import dubproxy; 7 | import dubproxy.git; 8 | import dubproxy.options; 9 | 10 | @safe: 11 | 12 | unittest { 13 | DubProxyOptions opts; 14 | DubProxyFile dpf = fromFile("testproxyfile.json"); 15 | 16 | TagReturn[] tags = getTags(dpf.getPath("dubproxy"), TagKind.all, opts); 17 | string h = getHashFromVersion(tags, "v0.0.1"); 18 | 19 | h = getHashFromVersion(tags, "v0.0.2"); 20 | } 21 | 22 | unittest { 23 | DubProxyOptions opts; 24 | import std.algorithm.searching : canFind, endsWith; 25 | DubProxyFile dpf = fromFile("testproxyfile.json"); 26 | TagReturn[] tags = getTags(dpf.getPath("dubproxy"), TagKind.branch, opts); 27 | assert(canFind!(a => a.tag.endsWith("master"))(tags), 28 | format("%(%s\n%)", tags)); 29 | } 30 | 31 | unittest { 32 | DubProxyOptions opts; 33 | opts.ovrGF = OverrideGitFolder.yes; 34 | opts.ovrWTF = OverrideWorkTreeFolder.yes; 35 | DubProxyFile dpf = fromFile("testproxyfile.json"); 36 | 37 | string xlsxPath = dpf.getPath("udt_d"); 38 | cloneBare(xlsxPath, LocalGit.no, "CloneTmp/GitDir/udt_d", opts); 39 | TagReturn[] tags = getTags(xlsxPath, TagKind.tags, opts); 40 | 41 | foreach(tag; tags) { 42 | createWorkingTree("CloneTmp/GitDir/udt_d", tag, "udt_d", "CloneTmp", 43 | opts); 44 | } 45 | } 46 | 47 | /* 48 | unittest { 49 | DubProxyOptions opts; 50 | opts.ovrGF = OverrideGitFolder.yes; 51 | opts.ovrWTF = OverrideWorkTreeFolder.yes; 52 | DubProxyFile dpf = fromFile("code.json"); 53 | 54 | string xlsxPath = dpf.getPath("colored"); 55 | cloneBare(xlsxPath, LocalGit.no, "/home/burner/.dub/GitDir/colored", opts); 56 | TagReturn[] tags = getTags(xlsxPath, opts); 57 | 58 | foreach(tag; tags) { 59 | createWorkingTree("/home/burner/.dub/GitDir/colored", tag, "colored", 60 | "/home/burner/.dub/packages", opts); 61 | } 62 | }*/ 63 | -------------------------------------------------------------------------------- /source/dubproxy/options.d: -------------------------------------------------------------------------------- 1 | module dubproxy.options; 2 | 3 | import std.typecons : Flag; 4 | 5 | alias OverrideGitFolder = Flag!"OverrideGitFolder"; 6 | alias OverrideWorkTreeFolder = Flag!"OverrideWorkTreeFolder"; 7 | 8 | struct DubProxyOptions { 9 | OverrideGitFolder ovrGF; 10 | OverrideWorkTreeFolder ovrWTF; 11 | string pathToGit = "git"; 12 | string pathToDub = "dub"; 13 | bool noUserInteraction; 14 | } 15 | -------------------------------------------------------------------------------- /source/dubproxy/package.d: -------------------------------------------------------------------------------- 1 | module dubproxy; 2 | 3 | import std.array : empty; 4 | import std.typecons : nullable, Nullable; 5 | import std.json; 6 | import std.format : format, formattedWrite; 7 | import std.file : exists, readText; 8 | import std.exception : enforce; 9 | import std.stdio; 10 | 11 | @safe: 12 | 13 | struct DubProxyFile { 14 | string[string] packages; 15 | 16 | string getPath(string pkg) const { 17 | const string* pkgPath = pkg in this.packages; 18 | enforce(pkgPath, format!"No package with name '%s' found in DPF"(pkg)); 19 | return *pkgPath; 20 | } 21 | 22 | bool pkgExists(string pkg) const { 23 | return (pkg in this.packages) !is null; 24 | } 25 | 26 | void insertPath(string pkg, string path) { 27 | const(string)* oldPath = pkg in this.packages; 28 | enforce(oldPath is null, format!"Package '%s' already with path '%s'" 29 | (pkg, *oldPath)); 30 | this.packages[pkg] = path; 31 | } 32 | 33 | void updatePath(string pkg, string path) { 34 | enforce(this.pkgExists(pkg), format!"Package '%s' must exist for update" 35 | (pkg)); 36 | this.packages[pkg] = path; 37 | } 38 | 39 | void removePackage(string pkg) { 40 | enforce(this.pkgExists(pkg), format!"Package '%s' does not exist in DPF" 41 | (pkg)); 42 | this.packages.remove(pkg); 43 | } 44 | } 45 | 46 | DubProxyFile fromFile(string path) @safe { 47 | enforce(exists(path), format!"No DPF exists with path '%s'"( path)); 48 | return fromString(readText(path)); 49 | } 50 | 51 | DubProxyFile fromString(string jsonText) @safe { 52 | JSONValue j = parseJSON(jsonText); 53 | 54 | enforce(j.type == JSONType.object, "Parsed DPF top level is " 55 | ~ "not an object"); 56 | 57 | const(JSONValue)* pkg = "packages" in j; 58 | enforce(pkg !is null, "Key 'packages does not exist in parsed DPF"); 59 | enforce(pkg.type == JSONType.object, 60 | "Value of 'packages' must be object"); 61 | 62 | DubProxyFile ret; 63 | 64 | JSONValue pkgCopy = *pkg; 65 | 66 | () @trusted { 67 | foreach(string key, ref JSONValue value; pkgCopy) { 68 | enforce(value.type == JSONType.string, format! 69 | ("Value type to key '%s' was '%s', type string " 70 | ~ "was expected")(key, value.type)); 71 | ret.packages[key] = value.str(); 72 | } 73 | }(); 74 | 75 | return ret; 76 | } 77 | 78 | void toFile(const(DubProxyFile) dpf, string path) { 79 | auto f = File(path, "w"); 80 | toImpl(f.lockingTextWriter(), dpf); 81 | } 82 | 83 | string toString(const(DubProxyFile) dpf, string path) { 84 | import std.array : appender; 85 | auto app = appender!string(); 86 | toImpl(app, dpf); 87 | return app.data; 88 | } 89 | 90 | private void toImpl(LTW)(auto ref LTW ltw, const(DubProxyFile) dpf) { 91 | import std.algorithm.iteration : map, joiner; 92 | import std.algorithm.mutation : copy; 93 | 94 | formattedWrite(ltw, "{\n\t\"packages\" : {\n"); 95 | dpf.packages.byKeyValue() 96 | .map!(it => format!"\t\t\"%s\" : \"%s\""(it.key, it.value)) 97 | .joiner(",\n") 98 | .copy(ltw); 99 | formattedWrite(ltw, "\n\t}"); 100 | formattedWrite(ltw, "\n}"); 101 | } 102 | 103 | DubProxyFile getCodeDlangOrgCopy() { 104 | return parseCodeDlangOrgData(getCodeDlangOrgData()); 105 | } 106 | 107 | string getCodeDlangOrgData() @trusted { 108 | import std.exception : assumeUnique; 109 | import std.net.curl; 110 | import std.zlib; 111 | 112 | auto data = get("https://code.dlang.org/api/packages/dump"); 113 | 114 | auto uc = new UnCompress(); 115 | 116 | const(void[]) un = uc.uncompress(data); 117 | return assumeUnique(cast(const(char)[])un); 118 | } 119 | 120 | DubProxyFile parseCodeDlangOrgData(string data) { 121 | string fixUpKind(string kind) { 122 | switch(kind) { 123 | case "github": return "github.com"; 124 | case "bitbucket": return "bitbucket.org"; 125 | case "gitlab": return "gitlab.com"; 126 | default: 127 | assert(false, kind); 128 | } 129 | } 130 | 131 | JSONValue parsed = parseJSON(data); 132 | DubProxyFile ret; 133 | 134 | enforce(parsed.type == JSONType.array, 135 | "Downloaded code.dlang.org dump was not an array"); 136 | 137 | foreach(it; parsed.arrayNoRef()) { 138 | enforce(it.type == JSONType.object, 139 | format!"Expected object got '%s' from '%s'"(it.type, 140 | it.toPrettyString())); 141 | auto name = "name" in it; 142 | enforce(name && name.type == JSONType.string, 143 | format!"no name found in '%s'"(it.toPrettyString())); 144 | string nameStr = name.str; 145 | //write(nameStr, " : "); 146 | auto repo = "repository" in it; 147 | if(repo && repo.type == JSONType.object) { 148 | auto kind = "kind" in (*repo); 149 | auto owner = "owner" in (*repo); 150 | auto project = "project" in (*repo); 151 | 152 | enforce(kind && kind.type == JSONType.string, 153 | format!"kind was null in '%s'" (repo.toPrettyString())); 154 | enforce(owner && owner.type == JSONType.string, 155 | format!"owner was null in '%s'" (repo.toPrettyString())); 156 | enforce(project && project.type == JSONType.string, 157 | format!"project was null in '%s'" (repo.toPrettyString())); 158 | 159 | string url = format!"https://%s/%s/%s.git"(fixUpKind(kind.str), 160 | owner.str, project.str); 161 | //writeln(url); 162 | ret.insertPath(nameStr, url); 163 | } 164 | } 165 | 166 | return ret; 167 | } 168 | -------------------------------------------------------------------------------- /source/dubproxy/parsetest.d: -------------------------------------------------------------------------------- 1 | module dubproxy.parsetest; 2 | 3 | import std.stdio; 4 | 5 | import dubproxy; 6 | import dubproxy.git; 7 | 8 | @safe: 9 | 10 | unittest { 11 | DubProxyFile dpf = fromFile("testproxyfile.json"); 12 | assert("xlsxd" in dpf.packages); 13 | assert("dubproxy" in dpf.packages); 14 | assert("udt_d" in dpf.packages); 15 | } 16 | 17 | /* 18 | unittest { 19 | import std.file : readText; 20 | string d = getCodeDlangOrgData(); 21 | DubProxyFile dpf = parseCodeDlangOrgData(d); 22 | toFile(dpf, "code.json"); 23 | 24 | DubProxyFile c = fromFile("code.json"); 25 | assert(dpf.packages == c.packages); 26 | }*/ 27 | -------------------------------------------------------------------------------- /testproxyfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages" : { 3 | "xlsxd" : "https://github.com/symmetryinvestments/xlsxd.git", 4 | "udt_d" : "https://github.com/symmetryinvestments/udt_d", 5 | "dubproxy" : "." 6 | } 7 | } 8 | --------------------------------------------------------------------------------