mnenv 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (306) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +3 -0
  3. data/PROPOSAL.md +197 -0
  4. data/README.adoc +168 -461
  5. data/Rakefile +7 -4
  6. data/bin/Install-Mnenv.ps1 +145 -0
  7. data/bin/mnenv-installer +72 -0
  8. data/completions/bash +47 -0
  9. data/completions/fish +29 -0
  10. data/completions/powershell.ps1 +94 -0
  11. data/completions/zsh +43 -0
  12. data/lib/mnenv/binary_repository.rb +189 -0
  13. data/lib/mnenv/chocolatey.rb +7 -0
  14. data/lib/mnenv/cli.rb +110 -10
  15. data/lib/mnenv/commands/available_command.rb +169 -0
  16. data/lib/mnenv/commands/chocolatey_command.rb +4 -5
  17. data/lib/mnenv/commands/gemfile_command.rb +4 -5
  18. data/lib/mnenv/commands/homebrew_command.rb +4 -5
  19. data/lib/mnenv/commands/install_command.rb +234 -0
  20. data/lib/mnenv/commands/snap_command.rb +5 -7
  21. data/lib/mnenv/commands/uninstall_command.rb +111 -0
  22. data/lib/mnenv/commands/version_command.rb +167 -0
  23. data/lib/mnenv/commands.rb +9 -4
  24. data/lib/mnenv/gemfile/extractor.rb +10 -3
  25. data/lib/mnenv/gemfile.rb +8 -0
  26. data/lib/mnenv/gemfile_repository.rb +0 -2
  27. data/lib/mnenv/homebrew.rb +7 -0
  28. data/lib/mnenv/installer/base.rb +62 -0
  29. data/lib/mnenv/installer/factory.rb +46 -0
  30. data/lib/mnenv/installer.rb +12 -0
  31. data/lib/mnenv/installers/binary_installer.rb +242 -0
  32. data/lib/mnenv/installers/gemfile_installer.rb +76 -0
  33. data/lib/mnenv/json_formatter.rb +3 -13
  34. data/lib/mnenv/logger.rb +9 -1
  35. data/lib/mnenv/models/binary_version.rb +78 -0
  36. data/lib/mnenv/models/chocolatey_version.rb +7 -0
  37. data/lib/mnenv/models/gemfile_version.rb +19 -5
  38. data/lib/mnenv/models/homebrew_version.rb +7 -0
  39. data/lib/mnenv/models/snap_version.rb +8 -0
  40. data/lib/mnenv/models/version.rb +16 -0
  41. data/lib/mnenv/models.rb +7 -5
  42. data/lib/mnenv/paths.rb +69 -0
  43. data/lib/mnenv/platform_detector.rb +109 -0
  44. data/lib/mnenv/repository.rb +50 -35
  45. data/lib/mnenv/resolver +72 -0
  46. data/lib/mnenv/shells/base.rb +32 -0
  47. data/lib/mnenv/shells/bash.rb +72 -0
  48. data/lib/mnenv/shells/cmd.rb +108 -0
  49. data/lib/mnenv/shells/factory.rb +82 -0
  50. data/lib/mnenv/shells/power_shell.rb +110 -0
  51. data/lib/mnenv/shim_manager.rb +121 -0
  52. data/lib/mnenv/snap.rb +7 -0
  53. data/lib/mnenv/snap_repository.rb +2 -19
  54. data/lib/mnenv/source_registry.rb +69 -0
  55. data/lib/mnenv/sources.rb +46 -0
  56. data/lib/mnenv/version.rb +1 -1
  57. data/lib/mnenv/version_resolver.rb +108 -0
  58. data/lib/mnenv/versions_manager.rb +92 -0
  59. data/lib/mnenv.rb +6 -0
  60. data/mnenv.gemspec +4 -1
  61. data/scripts/cross-source-switching-test.sh +214 -0
  62. data/scripts/integration-test.sh +89 -0
  63. data/scripts/version-switching-test.sh +151 -0
  64. metadata +85 -247
  65. data/data/chocolatey/versions.yaml +0 -812
  66. data/data/gemfile/v1.1.6/Gemfile +0 -4
  67. data/data/gemfile/v1.1.6/Gemfile.lock.archived +0 -232
  68. data/data/gemfile/v1.1.7/Gemfile +0 -4
  69. data/data/gemfile/v1.1.7/Gemfile.lock.archived +0 -235
  70. data/data/gemfile/v1.1.8/Gemfile +0 -4
  71. data/data/gemfile/v1.1.8/Gemfile.lock.archived +0 -238
  72. data/data/gemfile/v1.10.0/Gemfile +0 -5
  73. data/data/gemfile/v1.10.0/Gemfile.lock.archived +0 -930
  74. data/data/gemfile/v1.10.1/Gemfile +0 -5
  75. data/data/gemfile/v1.10.1/Gemfile.lock.archived +0 -929
  76. data/data/gemfile/v1.10.10/Gemfile +0 -5
  77. data/data/gemfile/v1.10.10/Gemfile.lock.archived +0 -973
  78. data/data/gemfile/v1.10.11/Gemfile +0 -5
  79. data/data/gemfile/v1.10.11/Gemfile.lock.archived +0 -975
  80. data/data/gemfile/v1.10.2/Gemfile +0 -5
  81. data/data/gemfile/v1.10.2/Gemfile.lock.archived +0 -939
  82. data/data/gemfile/v1.10.3/Gemfile +0 -5
  83. data/data/gemfile/v1.10.3/Gemfile.lock.archived +0 -946
  84. data/data/gemfile/v1.10.5/Gemfile +0 -5
  85. data/data/gemfile/v1.10.5/Gemfile.lock.archived +0 -958
  86. data/data/gemfile/v1.10.6/Gemfile +0 -5
  87. data/data/gemfile/v1.10.6/Gemfile.lock.archived +0 -969
  88. data/data/gemfile/v1.10.7/Gemfile +0 -5
  89. data/data/gemfile/v1.10.7/Gemfile.lock.archived +0 -969
  90. data/data/gemfile/v1.10.8/Gemfile +0 -5
  91. data/data/gemfile/v1.10.8/Gemfile.lock.archived +0 -968
  92. data/data/gemfile/v1.10.9/Gemfile +0 -5
  93. data/data/gemfile/v1.10.9/Gemfile.lock.archived +0 -972
  94. data/data/gemfile/v1.11.0/Gemfile +0 -5
  95. data/data/gemfile/v1.11.0/Gemfile.lock.archived +0 -971
  96. data/data/gemfile/v1.11.1/Gemfile +0 -5
  97. data/data/gemfile/v1.11.1/Gemfile.lock.archived +0 -975
  98. data/data/gemfile/v1.11.4/Gemfile +0 -5
  99. data/data/gemfile/v1.11.4/Gemfile.lock.archived +0 -1046
  100. data/data/gemfile/v1.11.5/Gemfile +0 -5
  101. data/data/gemfile/v1.11.5/Gemfile.lock.archived +0 -1047
  102. data/data/gemfile/v1.12.10/Gemfile +0 -3
  103. data/data/gemfile/v1.12.10/Gemfile.lock.archived +0 -1073
  104. data/data/gemfile/v1.12.3/Gemfile +0 -3
  105. data/data/gemfile/v1.12.3/Gemfile.lock.archived +0 -1050
  106. data/data/gemfile/v1.12.4/Gemfile +0 -3
  107. data/data/gemfile/v1.12.4/Gemfile.lock.archived +0 -1056
  108. data/data/gemfile/v1.12.5/Gemfile +0 -3
  109. data/data/gemfile/v1.12.5/Gemfile.lock.archived +0 -1054
  110. data/data/gemfile/v1.12.6/Gemfile +0 -3
  111. data/data/gemfile/v1.12.6/Gemfile.lock.archived +0 -1056
  112. data/data/gemfile/v1.12.8/Gemfile +0 -3
  113. data/data/gemfile/v1.12.8/Gemfile.lock.archived +0 -1063
  114. data/data/gemfile/v1.13.0/Gemfile +0 -3
  115. data/data/gemfile/v1.13.0/Gemfile.lock.archived +0 -1074
  116. data/data/gemfile/v1.13.2/Gemfile +0 -3
  117. data/data/gemfile/v1.13.2/Gemfile.lock.archived +0 -899
  118. data/data/gemfile/v1.13.3/Gemfile +0 -3
  119. data/data/gemfile/v1.13.3/Gemfile.lock.archived +0 -938
  120. data/data/gemfile/v1.13.4/Gemfile +0 -3
  121. data/data/gemfile/v1.13.4/Gemfile.lock.archived +0 -938
  122. data/data/gemfile/v1.13.5/Gemfile +0 -3
  123. data/data/gemfile/v1.13.5/Gemfile.lock.archived +0 -944
  124. data/data/gemfile/v1.13.7/Gemfile +0 -3
  125. data/data/gemfile/v1.13.7/Gemfile.lock.archived +0 -944
  126. data/data/gemfile/v1.13.8/Gemfile +0 -3
  127. data/data/gemfile/v1.13.8/Gemfile.lock.archived +0 -944
  128. data/data/gemfile/v1.13.9/Gemfile +0 -3
  129. data/data/gemfile/v1.13.9/Gemfile.lock.archived +0 -956
  130. data/data/gemfile/v1.14.3/Gemfile +0 -3
  131. data/data/gemfile/v1.14.3/Gemfile.lock.archived +0 -950
  132. data/data/gemfile/v1.2.12/Gemfile +0 -3
  133. data/data/gemfile/v1.2.12/Gemfile.lock.archived +0 -283
  134. data/data/gemfile/v1.2.2/Gemfile +0 -4
  135. data/data/gemfile/v1.2.2/Gemfile.lock.archived +0 -224
  136. data/data/gemfile/v1.2.3/Gemfile +0 -4
  137. data/data/gemfile/v1.2.3/Gemfile.lock.archived +0 -231
  138. data/data/gemfile/v1.2.6/Gemfile +0 -4
  139. data/data/gemfile/v1.2.6/Gemfile.lock.archived +0 -239
  140. data/data/gemfile/v1.2.8/Gemfile +0 -4
  141. data/data/gemfile/v1.2.8/Gemfile.lock.archived +0 -233
  142. data/data/gemfile/v1.2.9/Gemfile +0 -4
  143. data/data/gemfile/v1.2.9/Gemfile.lock.archived +0 -245
  144. data/data/gemfile/v1.3.1/Gemfile +0 -3
  145. data/data/gemfile/v1.3.1/Gemfile.lock.archived +0 -296
  146. data/data/gemfile/v1.3.2/Gemfile +0 -3
  147. data/data/gemfile/v1.3.2/Gemfile.lock.archived +0 -296
  148. data/data/gemfile/v1.3.4/Gemfile +0 -3
  149. data/data/gemfile/v1.3.4/Gemfile.lock.archived +0 -284
  150. data/data/gemfile/v1.3.5/Gemfile +0 -3
  151. data/data/gemfile/v1.3.5/Gemfile.lock.archived +0 -284
  152. data/data/gemfile/v1.3.6/Gemfile +0 -3
  153. data/data/gemfile/v1.3.6/Gemfile.lock.archived +0 -286
  154. data/data/gemfile/v1.3.9/Gemfile +0 -3
  155. data/data/gemfile/v1.3.9/Gemfile.lock.archived +0 -334
  156. data/data/gemfile/v1.4.0/Gemfile +0 -3
  157. data/data/gemfile/v1.4.0/Gemfile.lock.archived +0 -330
  158. data/data/gemfile/v1.4.10/Gemfile +0 -4
  159. data/data/gemfile/v1.4.10/Gemfile.lock.archived +0 -461
  160. data/data/gemfile/v1.4.11/Gemfile +0 -4
  161. data/data/gemfile/v1.4.11/Gemfile.lock.archived +0 -452
  162. data/data/gemfile/v1.4.12/Gemfile +0 -4
  163. data/data/gemfile/v1.4.12/Gemfile.lock.archived +0 -452
  164. data/data/gemfile/v1.4.13/Gemfile +0 -4
  165. data/data/gemfile/v1.4.13/Gemfile.lock.archived +0 -455
  166. data/data/gemfile/v1.4.14/Gemfile +0 -4
  167. data/data/gemfile/v1.4.14/Gemfile.lock.archived +0 -456
  168. data/data/gemfile/v1.4.18/Gemfile +0 -3
  169. data/data/gemfile/v1.4.18/Gemfile.lock.archived +0 -486
  170. data/data/gemfile/v1.4.3/Gemfile +0 -3
  171. data/data/gemfile/v1.4.3/Gemfile.lock.archived +0 -339
  172. data/data/gemfile/v1.4.4/Gemfile +0 -3
  173. data/data/gemfile/v1.4.4/Gemfile.lock.archived +0 -339
  174. data/data/gemfile/v1.4.5/Gemfile +0 -3
  175. data/data/gemfile/v1.4.5/Gemfile.lock.archived +0 -348
  176. data/data/gemfile/v1.4.6/Gemfile +0 -3
  177. data/data/gemfile/v1.4.6/Gemfile.lock.archived +0 -357
  178. data/data/gemfile/v1.4.7/Gemfile +0 -3
  179. data/data/gemfile/v1.4.7/Gemfile.lock.archived +0 -391
  180. data/data/gemfile/v1.4.8/Gemfile +0 -3
  181. data/data/gemfile/v1.4.8/Gemfile.lock.archived +0 -445
  182. data/data/gemfile/v1.4.9/Gemfile +0 -3
  183. data/data/gemfile/v1.4.9/Gemfile.lock.archived +0 -448
  184. data/data/gemfile/v1.5.0/Gemfile +0 -3
  185. data/data/gemfile/v1.5.0/Gemfile.lock.archived +0 -478
  186. data/data/gemfile/v1.5.10/Gemfile +0 -3
  187. data/data/gemfile/v1.5.10/Gemfile.lock.archived +0 -668
  188. data/data/gemfile/v1.5.11/Gemfile +0 -3
  189. data/data/gemfile/v1.5.11/Gemfile.lock.archived +0 -668
  190. data/data/gemfile/v1.5.15/Gemfile +0 -3
  191. data/data/gemfile/v1.5.15/Gemfile.lock.archived +0 -686
  192. data/data/gemfile/v1.5.16/Gemfile +0 -3
  193. data/data/gemfile/v1.5.16/Gemfile.lock.archived +0 -684
  194. data/data/gemfile/v1.5.17/Gemfile +0 -3
  195. data/data/gemfile/v1.5.17/Gemfile.lock.archived +0 -684
  196. data/data/gemfile/v1.5.18/Gemfile +0 -5
  197. data/data/gemfile/v1.5.18/Gemfile.lock.archived +0 -691
  198. data/data/gemfile/v1.5.19/Gemfile +0 -5
  199. data/data/gemfile/v1.5.19/Gemfile.lock.archived +0 -703
  200. data/data/gemfile/v1.5.20/Gemfile +0 -5
  201. data/data/gemfile/v1.5.20/Gemfile.lock.archived +0 -703
  202. data/data/gemfile/v1.5.21/Gemfile +0 -5
  203. data/data/gemfile/v1.5.21/Gemfile.lock.archived +0 -707
  204. data/data/gemfile/v1.5.22/Gemfile +0 -5
  205. data/data/gemfile/v1.5.22/Gemfile.lock.archived +0 -707
  206. data/data/gemfile/v1.5.23/Gemfile +0 -5
  207. data/data/gemfile/v1.5.23/Gemfile.lock.archived +0 -711
  208. data/data/gemfile/v1.5.24/Gemfile +0 -5
  209. data/data/gemfile/v1.5.24/Gemfile.lock.archived +0 -711
  210. data/data/gemfile/v1.5.3/Gemfile +0 -3
  211. data/data/gemfile/v1.5.3/Gemfile.lock.archived +0 -651
  212. data/data/gemfile/v1.5.4/Gemfile +0 -3
  213. data/data/gemfile/v1.5.4/Gemfile.lock.archived +0 -657
  214. data/data/gemfile/v1.5.5/Gemfile +0 -3
  215. data/data/gemfile/v1.5.5/Gemfile.lock.archived +0 -657
  216. data/data/gemfile/v1.5.6/Gemfile +0 -3
  217. data/data/gemfile/v1.5.6/Gemfile.lock.archived +0 -657
  218. data/data/gemfile/v1.5.7/Gemfile +0 -3
  219. data/data/gemfile/v1.5.7/Gemfile.lock.archived +0 -657
  220. data/data/gemfile/v1.5.8/Gemfile +0 -3
  221. data/data/gemfile/v1.5.8/Gemfile.lock.archived +0 -655
  222. data/data/gemfile/v1.5.9/Gemfile +0 -3
  223. data/data/gemfile/v1.5.9/Gemfile.lock.archived +0 -656
  224. data/data/gemfile/v1.6.1/Gemfile +0 -5
  225. data/data/gemfile/v1.6.1/Gemfile.lock.archived +0 -721
  226. data/data/gemfile/v1.6.10/Gemfile +0 -5
  227. data/data/gemfile/v1.6.10/Gemfile.lock.archived +0 -744
  228. data/data/gemfile/v1.6.11/Gemfile +0 -5
  229. data/data/gemfile/v1.6.11/Gemfile.lock.archived +0 -744
  230. data/data/gemfile/v1.6.12/Gemfile +0 -5
  231. data/data/gemfile/v1.6.12/Gemfile.lock.archived +0 -745
  232. data/data/gemfile/v1.6.13/Gemfile +0 -5
  233. data/data/gemfile/v1.6.13/Gemfile.lock.archived +0 -745
  234. data/data/gemfile/v1.6.14/Gemfile +0 -5
  235. data/data/gemfile/v1.6.14/Gemfile.lock.archived +0 -754
  236. data/data/gemfile/v1.6.15/Gemfile +0 -5
  237. data/data/gemfile/v1.6.15/Gemfile.lock.archived +0 -757
  238. data/data/gemfile/v1.6.2/Gemfile +0 -5
  239. data/data/gemfile/v1.6.2/Gemfile.lock.archived +0 -718
  240. data/data/gemfile/v1.6.3/Gemfile +0 -5
  241. data/data/gemfile/v1.6.3/Gemfile.lock.archived +0 -728
  242. data/data/gemfile/v1.6.4/Gemfile +0 -5
  243. data/data/gemfile/v1.6.4/Gemfile.lock.archived +0 -730
  244. data/data/gemfile/v1.6.5/Gemfile +0 -5
  245. data/data/gemfile/v1.6.5/Gemfile.lock.archived +0 -733
  246. data/data/gemfile/v1.6.6/Gemfile +0 -5
  247. data/data/gemfile/v1.6.6/Gemfile.lock.archived +0 -733
  248. data/data/gemfile/v1.6.7/Gemfile +0 -5
  249. data/data/gemfile/v1.6.7/Gemfile.lock.archived +0 -733
  250. data/data/gemfile/v1.6.9/Gemfile +0 -5
  251. data/data/gemfile/v1.6.9/Gemfile.lock.archived +0 -744
  252. data/data/gemfile/v1.7.0/Gemfile +0 -5
  253. data/data/gemfile/v1.7.0/Gemfile.lock.archived +0 -750
  254. data/data/gemfile/v1.7.1/Gemfile +0 -5
  255. data/data/gemfile/v1.7.1/Gemfile.lock.archived +0 -750
  256. data/data/gemfile/v1.7.2/Gemfile +0 -5
  257. data/data/gemfile/v1.7.2/Gemfile.lock.archived +0 -747
  258. data/data/gemfile/v1.7.3/Gemfile +0 -5
  259. data/data/gemfile/v1.7.3/Gemfile.lock.archived +0 -755
  260. data/data/gemfile/v1.7.4/Gemfile +0 -5
  261. data/data/gemfile/v1.7.4/Gemfile.lock.archived +0 -756
  262. data/data/gemfile/v1.7.5/Gemfile +0 -5
  263. data/data/gemfile/v1.7.5/Gemfile.lock.archived +0 -759
  264. data/data/gemfile/v1.7.6/Gemfile +0 -5
  265. data/data/gemfile/v1.7.6/Gemfile.lock.archived +0 -768
  266. data/data/gemfile/v1.8.10/Gemfile +0 -5
  267. data/data/gemfile/v1.8.10/Gemfile.lock.archived +0 -792
  268. data/data/gemfile/v1.8.11/Gemfile +0 -5
  269. data/data/gemfile/v1.8.11/Gemfile.lock.archived +0 -862
  270. data/data/gemfile/v1.8.3/Gemfile +0 -5
  271. data/data/gemfile/v1.8.3/Gemfile.lock.archived +0 -773
  272. data/data/gemfile/v1.8.4/Gemfile +0 -5
  273. data/data/gemfile/v1.8.4/Gemfile.lock.archived +0 -768
  274. data/data/gemfile/v1.8.5/Gemfile +0 -5
  275. data/data/gemfile/v1.8.5/Gemfile.lock.archived +0 -768
  276. data/data/gemfile/v1.8.6/Gemfile +0 -5
  277. data/data/gemfile/v1.8.6/Gemfile.lock.archived +0 -777
  278. data/data/gemfile/v1.8.7/Gemfile +0 -5
  279. data/data/gemfile/v1.8.7/Gemfile.lock.archived +0 -777
  280. data/data/gemfile/v1.8.8/Gemfile +0 -5
  281. data/data/gemfile/v1.8.8/Gemfile.lock.archived +0 -778
  282. data/data/gemfile/v1.8.9/Gemfile +0 -5
  283. data/data/gemfile/v1.8.9/Gemfile.lock.archived +0 -775
  284. data/data/gemfile/v1.9.0/Gemfile +0 -5
  285. data/data/gemfile/v1.9.0/Gemfile.lock.archived +0 -871
  286. data/data/gemfile/v1.9.1/Gemfile +0 -5
  287. data/data/gemfile/v1.9.1/Gemfile.lock.archived +0 -906
  288. data/data/gemfile/v1.9.2/Gemfile +0 -5
  289. data/data/gemfile/v1.9.2/Gemfile.lock.archived +0 -898
  290. data/data/gemfile/v1.9.3/Gemfile +0 -5
  291. data/data/gemfile/v1.9.3/Gemfile.lock.archived +0 -898
  292. data/data/gemfile/v1.9.4/Gemfile +0 -5
  293. data/data/gemfile/v1.9.4/Gemfile.lock.archived +0 -901
  294. data/data/gemfile/v1.9.5/Gemfile +0 -5
  295. data/data/gemfile/v1.9.5/Gemfile.lock.archived +0 -903
  296. data/data/gemfile/v1.9.6/Gemfile +0 -5
  297. data/data/gemfile/v1.9.6/Gemfile.lock.archived +0 -900
  298. data/data/gemfile/v1.9.7/Gemfile +0 -5
  299. data/data/gemfile/v1.9.7/Gemfile.lock.archived +0 -922
  300. data/data/gemfile/v1.9.8/Gemfile +0 -5
  301. data/data/gemfile/v1.9.8/Gemfile.lock.archived +0 -933
  302. data/data/gemfile/versions.yaml +0 -751
  303. data/data/homebrew/versions.yaml +0 -567
  304. data/data/snap/github_tags.json +0 -42
  305. data/data/snap/versions.yaml +0 -589
  306. data/snapcraft-list-copied-from-site.md +0 -101
@@ -0,0 +1,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open-uri'
4
+ require 'json'
5
+ require 'net/http'
6
+ require 'rubygems/package'
7
+ require 'zlib'
8
+ require 'tempfile'
9
+ require_relative '../installer/base'
10
+ require_relative '../binary_repository'
11
+
12
+ module Mnenv
13
+ module Installers
14
+ class BinaryInstaller < Installer
15
+ PACKED_MN_REPO = 'metanorma/packed-mn'
16
+ RELEASES_URL = "https://api.github.com/repos/#{PACKED_MN_REPO}/releases".freeze
17
+
18
+ def verify_prerequisites!
19
+ verify_version_available!
20
+ end
21
+
22
+ def perform_installation
23
+ download_binary
24
+ make_executable
25
+ end
26
+
27
+ def make_executable
28
+ # Linux/macOS: make the binary executable
29
+ binary_path = File.join(version_dir, 'metanorma')
30
+ FileUtils.chmod(0o755, binary_path) if File.exist?(binary_path)
31
+
32
+ # Windows: .exe files don't need chmod
33
+ exe_path = File.join(version_dir, 'metanorma.exe')
34
+ nil unless File.exist?(exe_path)
35
+ end
36
+
37
+ private
38
+
39
+ def verify_version_available!
40
+ repo = BinaryRepository.new
41
+ binary_version = repo.find(version)
42
+
43
+ return if binary_version
44
+
45
+ available = repo.all.map(&:version).first(5).join(', ')
46
+ raise InstallationError, "Binary version #{version} not found.\n" \
47
+ "Available: #{available}...\n" \
48
+ "Or use: mnenv install #{version} --source=gemfile"
49
+ end
50
+
51
+ def download_binary
52
+ url, format = binary_url_and_format
53
+ warn "Downloading #{url}..."
54
+
55
+ tempfile = download_to_tempfile(url)
56
+
57
+ case format
58
+ when 'tgz'
59
+ extract_tgz(tempfile)
60
+ when 'zip'
61
+ extract_zip(tempfile)
62
+ when 'exe'
63
+ # Windows .exe - just copy directly
64
+ FileUtils.cp(tempfile.path, File.join(version_dir, 'metanorma.exe'))
65
+ else
66
+ raise InstallationError, "Unknown binary format: #{format}"
67
+ end
68
+ rescue OpenURI::HTTPError => e
69
+ raise InstallationError, "Failed to download binary: #{e.message}"
70
+ ensure
71
+ tempfile&.close&.unlink
72
+ end
73
+
74
+ def binary_url_and_format
75
+ repo = BinaryRepository.new
76
+ binary_version = repo.find(version)
77
+
78
+ raise InstallationError, "Binary version #{version} not found in repository" unless binary_version
79
+
80
+ platform, arch, variant = detect_platform_arch_variant
81
+
82
+ # Try to find a matching platform in the version data
83
+ # Priority: exe for Windows, tgz for Unix, then zip as fallback
84
+ formats = platform == 'windows' ? %w[exe zip] : %w[tgz]
85
+
86
+ formats.each do |fmt|
87
+ platform_data = binary_version.find_platform(
88
+ name: platform,
89
+ arch: arch,
90
+ variant: variant,
91
+ format: fmt
92
+ )
93
+
94
+ return [platform_data['url'], fmt] if platform_data && platform_data['url']
95
+
96
+ # Try without variant (for non-musl systems)
97
+ platform_data = binary_version.find_platform(
98
+ name: platform,
99
+ arch: arch,
100
+ variant: nil,
101
+ format: fmt
102
+ )
103
+
104
+ return [platform_data['url'], fmt] if platform_data && platform_data['url']
105
+ end
106
+
107
+ # Fallback: construct URL manually (for backward compatibility)
108
+ warn 'Warning: Platform data not found in cache, constructing URL manually'
109
+ fallback_url_and_format(platform, arch, variant)
110
+ end
111
+
112
+ def fallback_url_and_format(platform, arch, variant)
113
+ tag_name = "v#{version}"
114
+
115
+ if platform == 'windows'
116
+ url_exe = "https://github.com/#{PACKED_MN_REPO}/releases/download/#{tag_name}/metanorma-#{platform}-#{arch}.exe"
117
+ return [url_exe, 'exe'] if url_exists?(url_exe)
118
+
119
+ url_zip = "https://github.com/#{PACKED_MN_REPO}/releases/download/#{tag_name}/metanorma-#{platform}-#{arch}.zip"
120
+ return [url_zip, 'zip'] if url_exists?(url_zip)
121
+
122
+ raise InstallationError, "No Windows binary found for version #{version}"
123
+ else
124
+ # Try with variant (e.g., musl)
125
+ if variant
126
+ url_variant = "https://github.com/#{PACKED_MN_REPO}/releases/download/#{tag_name}/metanorma-#{platform}-#{variant}-#{arch}.tgz"
127
+ return [url_variant, 'tgz'] if url_exists?(url_variant)
128
+ end
129
+
130
+ url_with_arch = "https://github.com/#{PACKED_MN_REPO}/releases/download/#{tag_name}/metanorma-#{platform}-#{arch}.tgz"
131
+ return [url_with_arch, 'tgz'] if url_exists?(url_with_arch)
132
+
133
+ raise InstallationError, "No binary found for #{platform}/#{arch} version #{version}"
134
+ end
135
+ end
136
+
137
+ def download_to_tempfile(url)
138
+ tempfile = Tempfile.new(['mnenv-binary', '.tmp'])
139
+ URI.open(url, 'rb') do |io|
140
+ IO.copy_stream(io, tempfile)
141
+ end
142
+ tempfile.rewind
143
+ tempfile
144
+ end
145
+
146
+ def extract_tgz(tempfile)
147
+ found = false
148
+ Gem::Package::TarReader.new(Zlib::GzipReader.open(tempfile.path)) do |tar|
149
+ tar.each do |entry|
150
+ next unless entry.file?
151
+
152
+ # Look for the metanorma binary
153
+ # The archive may contain:
154
+ # - metanorma (expected)
155
+ # - metanorma-linux-x86_64 (actual packed-mn naming)
156
+ # - metanorma-darwin-arm64
157
+ # etc.
158
+ filename = File.basename(entry.full_name)
159
+ next unless filename.start_with?('metanorma') && !filename.include?('.')
160
+
161
+ if filename == 'metanorma'
162
+ else
163
+ # Rename metanorma-linux-x86_64 to just metanorma
164
+ end
165
+ target_name = 'metanorma'
166
+
167
+ File.open(File.join(version_dir, target_name), 'wb') do |f|
168
+ f.write(entry.read)
169
+ end
170
+ found = true
171
+ break
172
+ end
173
+ end
174
+
175
+ raise InstallationError, 'Could not find metanorma binary in archive' unless found
176
+ end
177
+
178
+ def extract_zip(tempfile)
179
+ require 'zip'
180
+
181
+ Zip::File.open(tempfile.path) do |zip_file|
182
+ entry = zip_file.find { |e| File.basename(e.name) == 'metanorma.exe' }
183
+ raise InstallationError, 'Could not find metanorma.exe in zip archive' unless entry
184
+
185
+ File.open(File.join(version_dir, 'metanorma.exe'), 'wb') do |f|
186
+ f.write(entry.get_input_stream.read)
187
+ end
188
+ end
189
+ end
190
+
191
+ def url_exists?(url)
192
+ uri = URI(url)
193
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
194
+ response = http.head(uri.path)
195
+ response.is_a?(Net::HTTPSuccess)
196
+ end
197
+ rescue StandardError
198
+ false
199
+ end
200
+
201
+ def detect_platform_arch_variant
202
+ platform = case RbConfig::CONFIG['host_os']
203
+ when /linux/ then 'linux'
204
+ when /darwin/ then 'darwin'
205
+ when /mswin|mingw|cygwin/ then 'windows'
206
+ else raise InstallationError, 'Unsupported platform for binary installations'
207
+ end
208
+
209
+ # Detect architecture
210
+ arch = case RbConfig::CONFIG['host_cpu']
211
+ when /arm64|aarch64/ then 'arm64'
212
+ when /x86_64|x64/ then 'x86_64'
213
+ else 'x86_64' # Default to x86_64
214
+ end
215
+
216
+ # Detect variant (e.g., musl for Alpine Linux)
217
+ variant = detect_variant if platform == 'linux'
218
+
219
+ [platform, arch, variant]
220
+ end
221
+
222
+ def detect_variant
223
+ # Check for musl libc (Alpine Linux)
224
+ if File.exist?('/etc/alpine-release')
225
+ 'musl'
226
+ elsif File.symlink?('/lib/libc.musl-x86_64.so.1')
227
+ 'musl'
228
+ end
229
+ rescue StandardError
230
+ nil
231
+ end
232
+
233
+ def fetch_releases
234
+ URI(RELEASES_URL).open do |io|
235
+ JSON.parse(io.read)
236
+ end
237
+ rescue OpenURI::HTTPError => e
238
+ raise InstallationError, "Failed to fetch releases: #{e.message}"
239
+ end
240
+ end
241
+ end
242
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../installer/base'
4
+
5
+ module Mnenv
6
+ module Installers
7
+ class GemfileInstaller < Installer
8
+ def verify_prerequisites!
9
+ verify_version_exists!
10
+ verify_development_tools!
11
+ end
12
+
13
+ def perform_installation
14
+ copy_gemfiles
15
+ bundle_install
16
+ end
17
+
18
+ private
19
+
20
+ def verify_version_exists!
21
+ repo = GemfileRepository.new
22
+ return if repo.exists?(version)
23
+
24
+ available = repo.all.map(&:display_name).join(', ')
25
+ raise InstallationError, "Version #{version} not found in Gemfile repository. " \
26
+ "Available: #{available}"
27
+ end
28
+
29
+ def verify_development_tools!
30
+ required_tools = %w[ruby bundle]
31
+
32
+ # Use 'where' on Windows, 'which' on Unix
33
+ which_cmd = Gem.win_platform? ? 'where' : 'which'
34
+
35
+ missing_tools = required_tools.reject do |tool|
36
+ system(which_cmd, tool, out: File::NULL, err: File::NULL)
37
+ end
38
+
39
+ return if missing_tools.empty?
40
+
41
+ raise DevelopmentToolsMissing,
42
+ "Development tools required for Gemfile installation.\n" \
43
+ "Missing: #{missing_tools.join(', ')}\n" \
44
+ "Install with: apt-get install ruby bundler build-essential # Debian/Ubuntu\n" \
45
+ " brew install ruby bundler # macOS\n" \
46
+ "Or use: mnenv install #{version} --source=binary"
47
+ end
48
+
49
+ def copy_gemfiles
50
+ repo = GemfileRepository.new
51
+ version_obj = repo.find(version)
52
+
53
+ raise InstallationError, "Version #{version} not found" unless version_obj
54
+
55
+ gemfile_source = version_obj.gemfile_path_calc
56
+ gemfile_lock_source = version_obj.gemfile_lock_path_calc
57
+
58
+ raise InstallationError, "Gemfile not found for #{version}" unless File.exist?(gemfile_source)
59
+ raise InstallationError, "Gemfile.lock not found for #{version}" unless File.exist?(gemfile_lock_source)
60
+
61
+ FileUtils.cp(gemfile_source, File.join(version_dir, 'Gemfile'))
62
+ FileUtils.cp(gemfile_lock_source, File.join(version_dir, 'Gemfile.lock'))
63
+ end
64
+
65
+ def bundle_install
66
+ Dir.chdir(version_dir) do
67
+ # Use system with bundler for proper isolation
68
+ # Don't suppress output to help debug issues
69
+ unless system('bundle', 'install', '--path', '.bundle', '--binstubs', 'bin')
70
+ raise InstallationError, "Bundle install failed for #{version}"
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -5,12 +5,9 @@ require 'json'
5
5
  module Mnenv
6
6
  class JsonFormatter
7
7
  def self.format_version(version)
8
- {
9
- 'version' => version.version,
10
- 'published_at' => format_timestamp(version.published_at),
11
- 'parsed_at' => format_timestamp(version.parsed_at),
8
+ version.to_hash.merge(
12
9
  'display_name' => version.display_name
13
- }.merge(version_specific_fields(version))
10
+ ).merge(version_specific_fields(version))
14
11
  end
15
12
 
16
13
  def self.format_versions(versions)
@@ -22,18 +19,11 @@ module Mnenv
22
19
  end
23
20
 
24
21
  class << self
25
- def format_timestamp(t) = t&.strftime('%Y-%m-%dT%H:%M:%SZ')
26
-
22
+ # Additional fields specific to JSON output format
27
23
  def version_specific_fields(version)
28
24
  case version
29
25
  when GemfileVersion
30
26
  { 'gemfile_exists' => version.exists_locally? }
31
- when SnapVersion
32
- { 'revision' => version.revision, 'channel' => version.channel }
33
- when HomebrewVersion
34
- { 'tag_name' => version.tag_name, 'commit_sha' => version.commit_sha }
35
- when ChocolateyVersion
36
- { 'package_name' => version.package_name, 'is_pre_release' => version.is_pre_release }
37
27
  else
38
28
  {}
39
29
  end
data/lib/mnenv/logger.rb CHANGED
@@ -10,6 +10,7 @@ module Mnenv
10
10
  success: '✅',
11
11
  warning: '⚠️',
12
12
  error: '❌',
13
+ debug: '🔍',
13
14
  pulling: '📥',
14
15
  extracting: '📦',
15
16
  skipping: '⏭️',
@@ -33,6 +34,13 @@ module Mnenv
33
34
  warn Paint["#{EMOJIS[:error]} ERROR: #{message}", :red, :bold]
34
35
  end
35
36
 
37
+ # Only outputs when verbose mode is enabled
38
+ def debug(message)
39
+ return unless Cli.verbose?
40
+
41
+ puts Paint["#{EMOJIS[:debug]} [DEBUG] #{message}", :magenta]
42
+ end
43
+
36
44
  def pulling(version)
37
45
  puts Paint["#{EMOJIS[:pulling]} Pulling metanorma/metanorma:#{version}...", :blue]
38
46
  end
@@ -48,7 +56,7 @@ module Mnenv
48
56
  end
49
57
 
50
58
  def header(message)
51
- puts "\n" + Paint["=== #{message} ===", :bold, :white] + "\n"
59
+ puts "\n#{Paint["=== #{message} ===", :bold, :white]}\n"
52
60
  end
53
61
 
54
62
  def section(message)
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'version'
4
+
5
+ module Mnenv
6
+ class BinaryVersion < ArtifactVersion
7
+ attribute :metadata, :hash, default: {}
8
+
9
+ key_value do
10
+ map 'version', to: :version
11
+ map 'published_at', to: :published_at
12
+ map 'parsed_at', to: :parsed_at
13
+ map 'metadata', to: :metadata
14
+ end
15
+
16
+ # Binary versions use plain version display (no 'v' prefix)
17
+ def display_name = version
18
+
19
+ # Get the release tag name (with 'v' prefix)
20
+ def tag_name = "v#{version}"
21
+
22
+ # Get the GitHub release URL
23
+ def html_url
24
+ metadata&.dig('html_url')
25
+ end
26
+
27
+ # Get list of available assets for this release
28
+ def assets
29
+ metadata&.dig('assets') || []
30
+ end
31
+
32
+ # Get platforms info (with URLs, format, etc.)
33
+ def platforms
34
+ metadata&.dig('platforms') || []
35
+ end
36
+
37
+ # Check if binary is available for a specific platform
38
+ def binary_for_platform?(platform)
39
+ assets.any? { |a| a == "metanorma-#{platform}" }
40
+ end
41
+
42
+ # Find the best matching platform entry for current system
43
+ # Returns the platform hash with url, format, etc.
44
+ def find_platform(name:, arch:, variant: nil, format: nil)
45
+ candidates = platforms.select { |p| p['name'] == name && p['arch'] == arch }
46
+
47
+ # Filter by variant
48
+ # - If variant is specified, match platforms with that variant
49
+ # - If variant is nil, match platforms WITHOUT a variant (glibc, not musl)
50
+ candidates = if variant
51
+ candidates.select { |p| p['variant'] == variant }
52
+ else
53
+ candidates.select { |p| p['variant'].nil? }
54
+ end
55
+
56
+ # Filter by format if specified
57
+ candidates = candidates.select { |p| p['format'] == format } if format
58
+
59
+ candidates.first
60
+ end
61
+
62
+ # Get download URL for a specific platform/arch combination
63
+ def download_url(name:, arch:, variant: nil, format: nil)
64
+ platform = find_platform(name: name, arch: arch, variant: variant, format: format)
65
+ platform&.dig('url')
66
+ end
67
+
68
+ # Get available platform names
69
+ def available_platforms
70
+ platforms.map { |p| p['name'] }.uniq
71
+ end
72
+
73
+ # Get available architectures for a platform
74
+ def available_arches_for(platform_name)
75
+ platforms.select { |p| p['name'] == platform_name }.map { |p| p['arch'] }.uniq
76
+ end
77
+ end
78
+ end
@@ -14,5 +14,12 @@ module Mnenv
14
14
  map 'package_name', to: :package_name
15
15
  map 'is_pre_release', to: :is_pre_release
16
16
  end
17
+
18
+ def to_hash
19
+ super.merge(
20
+ 'package_name' => package_name,
21
+ 'is_pre_release' => is_pre_release
22
+ )
23
+ end
17
24
  end
18
25
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'version'
4
+ require_relative '../versions_manager'
4
5
 
5
6
  module Mnenv
6
7
  class GemfileVersion < ArtifactVersion
@@ -8,6 +9,15 @@ module Mnenv
8
9
  attribute :gemfile_path, :string
9
10
  attribute :gemfile_lock_path, :string
10
11
 
12
+ # Class-level versions manager for dependency injection in tests
13
+ class << self
14
+ def versions_manager
15
+ @versions_manager ||= VersionsManager.new
16
+ end
17
+
18
+ attr_writer :versions_manager
19
+ end
20
+
11
21
  key_value do
12
22
  map 'version', to: :version
13
23
  map 'published_at', to: :published_at
@@ -17,7 +27,9 @@ module Mnenv
17
27
  map 'gemfile_lock_path', to: :gemfile_lock_path
18
28
  end
19
29
 
20
- def data_dir = @data_dir ||= default_data_dir
30
+ def data_dir
31
+ @data_dir ||= File.join(self.class.versions_manager.data_path, 'gemfile')
32
+ end
21
33
 
22
34
  def directory_path = File.join(data_dir, "v#{version}")
23
35
 
@@ -31,10 +43,12 @@ module Mnenv
31
43
  File.file?(gemfile_lock_path_calc)
32
44
  end
33
45
 
34
- private
35
-
36
- def default_data_dir
37
- @default_data_dir ||= File.join(__dir__, '..', '..', '..', 'data', 'gemfile')
46
+ def to_hash
47
+ super.merge(
48
+ 'gemfile_exists' => gemfile_exists,
49
+ 'gemfile_path' => gemfile_path,
50
+ 'gemfile_lock_path' => gemfile_lock_path
51
+ )
38
52
  end
39
53
  end
40
54
  end
@@ -19,5 +19,12 @@ module Mnenv
19
19
  super
20
20
  @tag_name ||= "v#{version}" if version
21
21
  end
22
+
23
+ def to_hash
24
+ super.merge(
25
+ 'tag_name' => tag_name,
26
+ 'commit_sha' => commit_sha
27
+ )
28
+ end
22
29
  end
23
30
  end
@@ -18,5 +18,13 @@ module Mnenv
18
18
  end
19
19
 
20
20
  def display_name = revision ? "#{version}-#{revision}" : "v#{version}"
21
+
22
+ def to_hash
23
+ super.merge(
24
+ 'revision' => revision,
25
+ 'arch' => arch,
26
+ 'channel' => channel
27
+ )
28
+ end
21
29
  end
22
30
  end
@@ -16,6 +16,22 @@ module Mnenv
16
16
 
17
17
  def display_name = "v#{version}"
18
18
 
19
+ # Serialize version to hash for persistence
20
+ # Subclasses should override and merge with super
21
+ def to_hash
22
+ {
23
+ 'version' => version,
24
+ 'published_at' => format_timestamp(published_at),
25
+ 'parsed_at' => format_timestamp(parsed_at)
26
+ }
27
+ end
28
+
29
+ private
30
+
31
+ def format_timestamp(time)
32
+ time&.strftime('%Y-%m-%dT%H:%M:%SZ')
33
+ end
34
+
19
35
  protected
20
36
 
21
37
  def version_parts = version.split('.').map(&:to_i)
data/lib/mnenv/models.rb CHANGED
@@ -2,8 +2,10 @@
2
2
 
3
3
  require 'lutaml/model'
4
4
 
5
- require_relative 'models/version'
6
- require_relative 'models/gemfile_version'
7
- require_relative 'models/snap_version'
8
- require_relative 'models/homebrew_version'
9
- require_relative 'models/chocolatey_version'
5
+ module Mnenv
6
+ autoload :ArtifactVersion, 'mnenv/models/version'
7
+ autoload :GemfileVersion, 'mnenv/models/gemfile_version'
8
+ autoload :SnapVersion, 'mnenv/models/snap_version'
9
+ autoload :HomebrewVersion, 'mnenv/models/homebrew_version'
10
+ autoload :ChocolateyVersion, 'mnenv/models/chocolatey_version'
11
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mnenv
4
+ # Centralized directory paths for mnenv
5
+ # This ensures consistency across all components
6
+ module Paths
7
+ # Base directory for all mnenv data
8
+ MNENV_DIR = File.expand_path('~/.mnenv').freeze
9
+
10
+ # Directory for the git clone of metanorma/versions (READ-ONLY data)
11
+ # Contains: data/gemfile/versions.yaml, data/gemfile/v1.14.4/Gemfile, etc.
12
+ VERSIONS_DATA_DIR = File.join(MNENV_DIR, 'versions').freeze
13
+
14
+ # Directory for installed Metanorma versions
15
+ # Contains: 1.14.3-gemfile/, 1.14.3-binary/, 1.14.4-gemfile/, etc.
16
+ INSTALLED_DIR = File.join(MNENV_DIR, 'installed').freeze
17
+
18
+ # Directory for shim scripts
19
+ SHIMS_DIR = File.join(MNENV_DIR, 'shims').freeze
20
+
21
+ # Directory for mnenv library files (resolver, etc.)
22
+ LIB_DIR = File.join(MNENV_DIR, 'lib', 'mnenv').freeze
23
+
24
+ # Global version file
25
+ VERSION_FILE = File.join(MNENV_DIR, 'version').freeze
26
+
27
+ # Global source file
28
+ SOURCE_FILE = File.join(MNENV_DIR, 'source').freeze
29
+
30
+ class << self
31
+ # Ensure all required directories exist
32
+ def ensure_directories
33
+ FileUtils.mkdir_p(MNENV_DIR)
34
+ FileUtils.mkdir_p(INSTALLED_DIR)
35
+ FileUtils.mkdir_p(SHIMS_DIR)
36
+ FileUtils.mkdir_p(LIB_DIR)
37
+ end
38
+
39
+ # Get the installation directory for a specific version and source
40
+ # @param version [String] The version number (e.g., "1.14.4")
41
+ # @param source [String] The source type (e.g., "gemfile", "binary")
42
+ # @return [String] The full path to the installation directory
43
+ def version_install_dir(version, source = nil)
44
+ if source
45
+ File.join(INSTALLED_DIR, "#{version}-#{source}")
46
+ else
47
+ # Backward compatibility: if no source specified, use version only
48
+ File.join(INSTALLED_DIR, version)
49
+ end
50
+ end
51
+
52
+ # Get the path to the data directory within the versions repo
53
+ def versions_data_path
54
+ File.join(VERSIONS_DATA_DIR, 'data')
55
+ end
56
+
57
+ # Parse a directory name into version and source components
58
+ # @param dir_name [String] Directory name like "1.14.4-gemfile" or "1.14.4"
59
+ # @return [Array<String, String>] Tuple of [version, source] where source may be nil
60
+ def parse_version_dir(dir_name)
61
+ if dir_name =~ /^(.+)-(gemfile|binary)$/
62
+ [Regexp.last_match(1), Regexp.last_match(2)]
63
+ else
64
+ [dir_name, nil]
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end