mnenv 0.1.0 → 0.1.1

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 (303) 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 +42 -0
  22. data/lib/mnenv/commands/version_command.rb +213 -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 +17 -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/repository.rb +50 -35
  44. data/lib/mnenv/resolver +72 -0
  45. data/lib/mnenv/shells/base.rb +32 -0
  46. data/lib/mnenv/shells/bash.rb +72 -0
  47. data/lib/mnenv/shells/cmd.rb +108 -0
  48. data/lib/mnenv/shells/factory.rb +82 -0
  49. data/lib/mnenv/shells/power_shell.rb +110 -0
  50. data/lib/mnenv/shim_manager.rb +121 -0
  51. data/lib/mnenv/snap.rb +7 -0
  52. data/lib/mnenv/snap_repository.rb +2 -19
  53. data/lib/mnenv/source_registry.rb +69 -0
  54. data/lib/mnenv/version.rb +1 -1
  55. data/lib/mnenv/versions_manager.rb +92 -0
  56. data/lib/mnenv.rb +3 -0
  57. data/mnenv.gemspec +4 -1
  58. data/scripts/cross-source-switching-test.sh +214 -0
  59. data/scripts/integration-test.sh +89 -0
  60. data/scripts/version-switching-test.sh +151 -0
  61. metadata +82 -247
  62. data/data/chocolatey/versions.yaml +0 -812
  63. data/data/gemfile/v1.1.6/Gemfile +0 -4
  64. data/data/gemfile/v1.1.6/Gemfile.lock.archived +0 -232
  65. data/data/gemfile/v1.1.7/Gemfile +0 -4
  66. data/data/gemfile/v1.1.7/Gemfile.lock.archived +0 -235
  67. data/data/gemfile/v1.1.8/Gemfile +0 -4
  68. data/data/gemfile/v1.1.8/Gemfile.lock.archived +0 -238
  69. data/data/gemfile/v1.10.0/Gemfile +0 -5
  70. data/data/gemfile/v1.10.0/Gemfile.lock.archived +0 -930
  71. data/data/gemfile/v1.10.1/Gemfile +0 -5
  72. data/data/gemfile/v1.10.1/Gemfile.lock.archived +0 -929
  73. data/data/gemfile/v1.10.10/Gemfile +0 -5
  74. data/data/gemfile/v1.10.10/Gemfile.lock.archived +0 -973
  75. data/data/gemfile/v1.10.11/Gemfile +0 -5
  76. data/data/gemfile/v1.10.11/Gemfile.lock.archived +0 -975
  77. data/data/gemfile/v1.10.2/Gemfile +0 -5
  78. data/data/gemfile/v1.10.2/Gemfile.lock.archived +0 -939
  79. data/data/gemfile/v1.10.3/Gemfile +0 -5
  80. data/data/gemfile/v1.10.3/Gemfile.lock.archived +0 -946
  81. data/data/gemfile/v1.10.5/Gemfile +0 -5
  82. data/data/gemfile/v1.10.5/Gemfile.lock.archived +0 -958
  83. data/data/gemfile/v1.10.6/Gemfile +0 -5
  84. data/data/gemfile/v1.10.6/Gemfile.lock.archived +0 -969
  85. data/data/gemfile/v1.10.7/Gemfile +0 -5
  86. data/data/gemfile/v1.10.7/Gemfile.lock.archived +0 -969
  87. data/data/gemfile/v1.10.8/Gemfile +0 -5
  88. data/data/gemfile/v1.10.8/Gemfile.lock.archived +0 -968
  89. data/data/gemfile/v1.10.9/Gemfile +0 -5
  90. data/data/gemfile/v1.10.9/Gemfile.lock.archived +0 -972
  91. data/data/gemfile/v1.11.0/Gemfile +0 -5
  92. data/data/gemfile/v1.11.0/Gemfile.lock.archived +0 -971
  93. data/data/gemfile/v1.11.1/Gemfile +0 -5
  94. data/data/gemfile/v1.11.1/Gemfile.lock.archived +0 -975
  95. data/data/gemfile/v1.11.4/Gemfile +0 -5
  96. data/data/gemfile/v1.11.4/Gemfile.lock.archived +0 -1046
  97. data/data/gemfile/v1.11.5/Gemfile +0 -5
  98. data/data/gemfile/v1.11.5/Gemfile.lock.archived +0 -1047
  99. data/data/gemfile/v1.12.10/Gemfile +0 -3
  100. data/data/gemfile/v1.12.10/Gemfile.lock.archived +0 -1073
  101. data/data/gemfile/v1.12.3/Gemfile +0 -3
  102. data/data/gemfile/v1.12.3/Gemfile.lock.archived +0 -1050
  103. data/data/gemfile/v1.12.4/Gemfile +0 -3
  104. data/data/gemfile/v1.12.4/Gemfile.lock.archived +0 -1056
  105. data/data/gemfile/v1.12.5/Gemfile +0 -3
  106. data/data/gemfile/v1.12.5/Gemfile.lock.archived +0 -1054
  107. data/data/gemfile/v1.12.6/Gemfile +0 -3
  108. data/data/gemfile/v1.12.6/Gemfile.lock.archived +0 -1056
  109. data/data/gemfile/v1.12.8/Gemfile +0 -3
  110. data/data/gemfile/v1.12.8/Gemfile.lock.archived +0 -1063
  111. data/data/gemfile/v1.13.0/Gemfile +0 -3
  112. data/data/gemfile/v1.13.0/Gemfile.lock.archived +0 -1074
  113. data/data/gemfile/v1.13.2/Gemfile +0 -3
  114. data/data/gemfile/v1.13.2/Gemfile.lock.archived +0 -899
  115. data/data/gemfile/v1.13.3/Gemfile +0 -3
  116. data/data/gemfile/v1.13.3/Gemfile.lock.archived +0 -938
  117. data/data/gemfile/v1.13.4/Gemfile +0 -3
  118. data/data/gemfile/v1.13.4/Gemfile.lock.archived +0 -938
  119. data/data/gemfile/v1.13.5/Gemfile +0 -3
  120. data/data/gemfile/v1.13.5/Gemfile.lock.archived +0 -944
  121. data/data/gemfile/v1.13.7/Gemfile +0 -3
  122. data/data/gemfile/v1.13.7/Gemfile.lock.archived +0 -944
  123. data/data/gemfile/v1.13.8/Gemfile +0 -3
  124. data/data/gemfile/v1.13.8/Gemfile.lock.archived +0 -944
  125. data/data/gemfile/v1.13.9/Gemfile +0 -3
  126. data/data/gemfile/v1.13.9/Gemfile.lock.archived +0 -956
  127. data/data/gemfile/v1.14.3/Gemfile +0 -3
  128. data/data/gemfile/v1.14.3/Gemfile.lock.archived +0 -950
  129. data/data/gemfile/v1.2.12/Gemfile +0 -3
  130. data/data/gemfile/v1.2.12/Gemfile.lock.archived +0 -283
  131. data/data/gemfile/v1.2.2/Gemfile +0 -4
  132. data/data/gemfile/v1.2.2/Gemfile.lock.archived +0 -224
  133. data/data/gemfile/v1.2.3/Gemfile +0 -4
  134. data/data/gemfile/v1.2.3/Gemfile.lock.archived +0 -231
  135. data/data/gemfile/v1.2.6/Gemfile +0 -4
  136. data/data/gemfile/v1.2.6/Gemfile.lock.archived +0 -239
  137. data/data/gemfile/v1.2.8/Gemfile +0 -4
  138. data/data/gemfile/v1.2.8/Gemfile.lock.archived +0 -233
  139. data/data/gemfile/v1.2.9/Gemfile +0 -4
  140. data/data/gemfile/v1.2.9/Gemfile.lock.archived +0 -245
  141. data/data/gemfile/v1.3.1/Gemfile +0 -3
  142. data/data/gemfile/v1.3.1/Gemfile.lock.archived +0 -296
  143. data/data/gemfile/v1.3.2/Gemfile +0 -3
  144. data/data/gemfile/v1.3.2/Gemfile.lock.archived +0 -296
  145. data/data/gemfile/v1.3.4/Gemfile +0 -3
  146. data/data/gemfile/v1.3.4/Gemfile.lock.archived +0 -284
  147. data/data/gemfile/v1.3.5/Gemfile +0 -3
  148. data/data/gemfile/v1.3.5/Gemfile.lock.archived +0 -284
  149. data/data/gemfile/v1.3.6/Gemfile +0 -3
  150. data/data/gemfile/v1.3.6/Gemfile.lock.archived +0 -286
  151. data/data/gemfile/v1.3.9/Gemfile +0 -3
  152. data/data/gemfile/v1.3.9/Gemfile.lock.archived +0 -334
  153. data/data/gemfile/v1.4.0/Gemfile +0 -3
  154. data/data/gemfile/v1.4.0/Gemfile.lock.archived +0 -330
  155. data/data/gemfile/v1.4.10/Gemfile +0 -4
  156. data/data/gemfile/v1.4.10/Gemfile.lock.archived +0 -461
  157. data/data/gemfile/v1.4.11/Gemfile +0 -4
  158. data/data/gemfile/v1.4.11/Gemfile.lock.archived +0 -452
  159. data/data/gemfile/v1.4.12/Gemfile +0 -4
  160. data/data/gemfile/v1.4.12/Gemfile.lock.archived +0 -452
  161. data/data/gemfile/v1.4.13/Gemfile +0 -4
  162. data/data/gemfile/v1.4.13/Gemfile.lock.archived +0 -455
  163. data/data/gemfile/v1.4.14/Gemfile +0 -4
  164. data/data/gemfile/v1.4.14/Gemfile.lock.archived +0 -456
  165. data/data/gemfile/v1.4.18/Gemfile +0 -3
  166. data/data/gemfile/v1.4.18/Gemfile.lock.archived +0 -486
  167. data/data/gemfile/v1.4.3/Gemfile +0 -3
  168. data/data/gemfile/v1.4.3/Gemfile.lock.archived +0 -339
  169. data/data/gemfile/v1.4.4/Gemfile +0 -3
  170. data/data/gemfile/v1.4.4/Gemfile.lock.archived +0 -339
  171. data/data/gemfile/v1.4.5/Gemfile +0 -3
  172. data/data/gemfile/v1.4.5/Gemfile.lock.archived +0 -348
  173. data/data/gemfile/v1.4.6/Gemfile +0 -3
  174. data/data/gemfile/v1.4.6/Gemfile.lock.archived +0 -357
  175. data/data/gemfile/v1.4.7/Gemfile +0 -3
  176. data/data/gemfile/v1.4.7/Gemfile.lock.archived +0 -391
  177. data/data/gemfile/v1.4.8/Gemfile +0 -3
  178. data/data/gemfile/v1.4.8/Gemfile.lock.archived +0 -445
  179. data/data/gemfile/v1.4.9/Gemfile +0 -3
  180. data/data/gemfile/v1.4.9/Gemfile.lock.archived +0 -448
  181. data/data/gemfile/v1.5.0/Gemfile +0 -3
  182. data/data/gemfile/v1.5.0/Gemfile.lock.archived +0 -478
  183. data/data/gemfile/v1.5.10/Gemfile +0 -3
  184. data/data/gemfile/v1.5.10/Gemfile.lock.archived +0 -668
  185. data/data/gemfile/v1.5.11/Gemfile +0 -3
  186. data/data/gemfile/v1.5.11/Gemfile.lock.archived +0 -668
  187. data/data/gemfile/v1.5.15/Gemfile +0 -3
  188. data/data/gemfile/v1.5.15/Gemfile.lock.archived +0 -686
  189. data/data/gemfile/v1.5.16/Gemfile +0 -3
  190. data/data/gemfile/v1.5.16/Gemfile.lock.archived +0 -684
  191. data/data/gemfile/v1.5.17/Gemfile +0 -3
  192. data/data/gemfile/v1.5.17/Gemfile.lock.archived +0 -684
  193. data/data/gemfile/v1.5.18/Gemfile +0 -5
  194. data/data/gemfile/v1.5.18/Gemfile.lock.archived +0 -691
  195. data/data/gemfile/v1.5.19/Gemfile +0 -5
  196. data/data/gemfile/v1.5.19/Gemfile.lock.archived +0 -703
  197. data/data/gemfile/v1.5.20/Gemfile +0 -5
  198. data/data/gemfile/v1.5.20/Gemfile.lock.archived +0 -703
  199. data/data/gemfile/v1.5.21/Gemfile +0 -5
  200. data/data/gemfile/v1.5.21/Gemfile.lock.archived +0 -707
  201. data/data/gemfile/v1.5.22/Gemfile +0 -5
  202. data/data/gemfile/v1.5.22/Gemfile.lock.archived +0 -707
  203. data/data/gemfile/v1.5.23/Gemfile +0 -5
  204. data/data/gemfile/v1.5.23/Gemfile.lock.archived +0 -711
  205. data/data/gemfile/v1.5.24/Gemfile +0 -5
  206. data/data/gemfile/v1.5.24/Gemfile.lock.archived +0 -711
  207. data/data/gemfile/v1.5.3/Gemfile +0 -3
  208. data/data/gemfile/v1.5.3/Gemfile.lock.archived +0 -651
  209. data/data/gemfile/v1.5.4/Gemfile +0 -3
  210. data/data/gemfile/v1.5.4/Gemfile.lock.archived +0 -657
  211. data/data/gemfile/v1.5.5/Gemfile +0 -3
  212. data/data/gemfile/v1.5.5/Gemfile.lock.archived +0 -657
  213. data/data/gemfile/v1.5.6/Gemfile +0 -3
  214. data/data/gemfile/v1.5.6/Gemfile.lock.archived +0 -657
  215. data/data/gemfile/v1.5.7/Gemfile +0 -3
  216. data/data/gemfile/v1.5.7/Gemfile.lock.archived +0 -657
  217. data/data/gemfile/v1.5.8/Gemfile +0 -3
  218. data/data/gemfile/v1.5.8/Gemfile.lock.archived +0 -655
  219. data/data/gemfile/v1.5.9/Gemfile +0 -3
  220. data/data/gemfile/v1.5.9/Gemfile.lock.archived +0 -656
  221. data/data/gemfile/v1.6.1/Gemfile +0 -5
  222. data/data/gemfile/v1.6.1/Gemfile.lock.archived +0 -721
  223. data/data/gemfile/v1.6.10/Gemfile +0 -5
  224. data/data/gemfile/v1.6.10/Gemfile.lock.archived +0 -744
  225. data/data/gemfile/v1.6.11/Gemfile +0 -5
  226. data/data/gemfile/v1.6.11/Gemfile.lock.archived +0 -744
  227. data/data/gemfile/v1.6.12/Gemfile +0 -5
  228. data/data/gemfile/v1.6.12/Gemfile.lock.archived +0 -745
  229. data/data/gemfile/v1.6.13/Gemfile +0 -5
  230. data/data/gemfile/v1.6.13/Gemfile.lock.archived +0 -745
  231. data/data/gemfile/v1.6.14/Gemfile +0 -5
  232. data/data/gemfile/v1.6.14/Gemfile.lock.archived +0 -754
  233. data/data/gemfile/v1.6.15/Gemfile +0 -5
  234. data/data/gemfile/v1.6.15/Gemfile.lock.archived +0 -757
  235. data/data/gemfile/v1.6.2/Gemfile +0 -5
  236. data/data/gemfile/v1.6.2/Gemfile.lock.archived +0 -718
  237. data/data/gemfile/v1.6.3/Gemfile +0 -5
  238. data/data/gemfile/v1.6.3/Gemfile.lock.archived +0 -728
  239. data/data/gemfile/v1.6.4/Gemfile +0 -5
  240. data/data/gemfile/v1.6.4/Gemfile.lock.archived +0 -730
  241. data/data/gemfile/v1.6.5/Gemfile +0 -5
  242. data/data/gemfile/v1.6.5/Gemfile.lock.archived +0 -733
  243. data/data/gemfile/v1.6.6/Gemfile +0 -5
  244. data/data/gemfile/v1.6.6/Gemfile.lock.archived +0 -733
  245. data/data/gemfile/v1.6.7/Gemfile +0 -5
  246. data/data/gemfile/v1.6.7/Gemfile.lock.archived +0 -733
  247. data/data/gemfile/v1.6.9/Gemfile +0 -5
  248. data/data/gemfile/v1.6.9/Gemfile.lock.archived +0 -744
  249. data/data/gemfile/v1.7.0/Gemfile +0 -5
  250. data/data/gemfile/v1.7.0/Gemfile.lock.archived +0 -750
  251. data/data/gemfile/v1.7.1/Gemfile +0 -5
  252. data/data/gemfile/v1.7.1/Gemfile.lock.archived +0 -750
  253. data/data/gemfile/v1.7.2/Gemfile +0 -5
  254. data/data/gemfile/v1.7.2/Gemfile.lock.archived +0 -747
  255. data/data/gemfile/v1.7.3/Gemfile +0 -5
  256. data/data/gemfile/v1.7.3/Gemfile.lock.archived +0 -755
  257. data/data/gemfile/v1.7.4/Gemfile +0 -5
  258. data/data/gemfile/v1.7.4/Gemfile.lock.archived +0 -756
  259. data/data/gemfile/v1.7.5/Gemfile +0 -5
  260. data/data/gemfile/v1.7.5/Gemfile.lock.archived +0 -759
  261. data/data/gemfile/v1.7.6/Gemfile +0 -5
  262. data/data/gemfile/v1.7.6/Gemfile.lock.archived +0 -768
  263. data/data/gemfile/v1.8.10/Gemfile +0 -5
  264. data/data/gemfile/v1.8.10/Gemfile.lock.archived +0 -792
  265. data/data/gemfile/v1.8.11/Gemfile +0 -5
  266. data/data/gemfile/v1.8.11/Gemfile.lock.archived +0 -862
  267. data/data/gemfile/v1.8.3/Gemfile +0 -5
  268. data/data/gemfile/v1.8.3/Gemfile.lock.archived +0 -773
  269. data/data/gemfile/v1.8.4/Gemfile +0 -5
  270. data/data/gemfile/v1.8.4/Gemfile.lock.archived +0 -768
  271. data/data/gemfile/v1.8.5/Gemfile +0 -5
  272. data/data/gemfile/v1.8.5/Gemfile.lock.archived +0 -768
  273. data/data/gemfile/v1.8.6/Gemfile +0 -5
  274. data/data/gemfile/v1.8.6/Gemfile.lock.archived +0 -777
  275. data/data/gemfile/v1.8.7/Gemfile +0 -5
  276. data/data/gemfile/v1.8.7/Gemfile.lock.archived +0 -777
  277. data/data/gemfile/v1.8.8/Gemfile +0 -5
  278. data/data/gemfile/v1.8.8/Gemfile.lock.archived +0 -778
  279. data/data/gemfile/v1.8.9/Gemfile +0 -5
  280. data/data/gemfile/v1.8.9/Gemfile.lock.archived +0 -775
  281. data/data/gemfile/v1.9.0/Gemfile +0 -5
  282. data/data/gemfile/v1.9.0/Gemfile.lock.archived +0 -871
  283. data/data/gemfile/v1.9.1/Gemfile +0 -5
  284. data/data/gemfile/v1.9.1/Gemfile.lock.archived +0 -906
  285. data/data/gemfile/v1.9.2/Gemfile +0 -5
  286. data/data/gemfile/v1.9.2/Gemfile.lock.archived +0 -898
  287. data/data/gemfile/v1.9.3/Gemfile +0 -5
  288. data/data/gemfile/v1.9.3/Gemfile.lock.archived +0 -898
  289. data/data/gemfile/v1.9.4/Gemfile +0 -5
  290. data/data/gemfile/v1.9.4/Gemfile.lock.archived +0 -901
  291. data/data/gemfile/v1.9.5/Gemfile +0 -5
  292. data/data/gemfile/v1.9.5/Gemfile.lock.archived +0 -903
  293. data/data/gemfile/v1.9.6/Gemfile +0 -5
  294. data/data/gemfile/v1.9.6/Gemfile.lock.archived +0 -900
  295. data/data/gemfile/v1.9.7/Gemfile +0 -5
  296. data/data/gemfile/v1.9.7/Gemfile.lock.archived +0 -922
  297. data/data/gemfile/v1.9.8/Gemfile +0 -5
  298. data/data/gemfile/v1.9.8/Gemfile.lock.archived +0 -933
  299. data/data/gemfile/versions.yaml +0 -751
  300. data/data/homebrew/versions.yaml +0 -567
  301. data/data/snap/github_tags.json +0 -42
  302. data/data/snap/versions.yaml +0 -589
  303. 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