summaryrefslogtreecommitdiff
path: root/xmake/modules/private/action/require/impl/install_packages.lua
blob: bd7d5960674665e6787a39e345ace33ac3f32298 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
--!A cross-platform build utility based on Lua
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
--     http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015-present, Xmake Open Source Community.
--
-- @author      ruki
-- @file        install_packages.lua
--

-- imports
import("core.base.option")
import("core.base.hashset")
import("core.base.scheduler")
import("core.project.project")
import("core.base.tty")
import("async.runjobs")
import("utils.waiting_indicator", {alias = "waiting_indicator"})
import("net.fasturl")
import("private.action.require.impl.package")
import("private.action.require.impl.lock_packages")
import("private.action.require.impl.register_packages")
import("private.action.require.impl.actions.check", {alias = "action_check"})
import("private.action.require.impl.actions.install", {alias = "action_install"})
import("private.action.require.impl.actions.download", {alias = "action_download"})

-- sort packages urls
function _sort_packages_urls(packages)

    -- add all urls to fasturl and prepare to sort them together
    for _, instance in pairs(packages) do
        fasturl.add(instance:urls())
    end

    -- sort and update urls
    for _, instance in pairs(packages) do
        instance:urls_set(fasturl.sort(instance:urls()))
    end
end

-- replace modified package
function _replace_package(packages, instance, extinstance)
    for idx, rawinstance in ipairs(packages) do
        if rawinstance == instance then
            packages[idx] = extinstance
        end
        local deps = rawinstance._DEPS
        for name, dep in pairs(deps) do
            if dep == instance then
                deps[name] = nil
                deps[extinstance:name()] = extinstance
                break
            end
        end
        local parents = rawinstance._PARENTS
        for name, parent in pairs(parents) do
            if parent == instance then
                parents[name] = nil
                parents[extinstance:name()] = extinstance
                break
            end
        end
        local orderdeps = rawinstance._ORDERDEPS
        for depidx, dep in ipairs(orderdeps) do
            if dep == instance then
                orderdeps[depidx] = extinstance
                break
            end
        end
        local librarydeps = rawinstance._LIBRARYDEPS
        for depidx, dep in ipairs(librarydeps) do
            if dep == instance then
                librarydeps[depidx] = extinstance
                break
            end
        end
        local plaindeps = rawinstance._PLAINDEPS
        for depidx, dep in ipairs(rawinstance._PLAINDEPS) do
            if dep == instance then
                plaindeps[depidx] = extinstance
                break
            end
        end
    end
end

-- replace modified packages
function _replace_packages(packages, packages_modified)
    for _, package_modified in ipairs(packages_modified) do
        local instance = package_modified.instance
        local extinstance = package_modified.extinstance
        _replace_package(packages, instance, extinstance)
    end
end

-- get user confirm from 3rd package sources
-- @see https://github.com/xmake-io/xmake/issues/1140
function _get_confirm_from_3rd(packages)

    -- get extpackages list
    local extpackages_list = _g.extpackages_list
    if not extpackages_list then
        extpackages_list = {}
        for _, instance in ipairs(packages) do
            local extsources = instance:get("extsources")
            local extsources_extra = instance:extraconf("extsources")
            if extsources then
                local extpackages = package.load_packages(extsources, extsources_extra)
                for _, extinstance in ipairs(extpackages) do
                    table.insert(extpackages_list, {instance = instance, extinstance = extinstance})
                end
            end
        end
        _g.extpackages_list = extpackages_list
    end

    -- no extpackages?
    if #extpackages_list == 0 then
        print("no more packages!")
        return
    end

    -- get confirm result
    local result = utils.confirm({description = function ()
        cprint("${bright color.warning}note: ${clear}select the following 3rd packages")
        for idx, extinstance in ipairs(extpackages_list) do
            local instance = extinstance.instance
            local extinstance = extinstance.extinstance
            cprint("  ${yellow}%d.${clear} %s ${yellow}->${clear} %s %s ${dim}%s",
                idx, extinstance:name(),
                instance:displayname(),
                instance:version_str() or "",
                package.get_configs_str(instance))
        end
    end, answer = function ()
        cprint("please input number list: ${bright}n${clear} (1,2,..)")
        io.flush()
        return (io.read() or "n"):trim()
    end})

    -- get confirmed extpackages
    local confirmed_extpackages = {}
    if result and result ~= "n" then
        for _, idx in ipairs(result:split(',')) do
            idx = tonumber(idx)
            if extpackages_list[idx] then
                table.insert(confirmed_extpackages, extpackages_list[idx])
            end
        end
    end

    -- modify packages
    if #confirmed_extpackages > 0 then
        _replace_packages(packages, confirmed_extpackages)
        return confirmed_extpackages
    end
end

-- get user confirm
function _get_confirm(packages, opt)
    opt = opt or {}

    -- no confirmed packages?
    if #packages == 0 then
        return true
    end

    local result
    local packages_modified
    while result == nil do
        -- get confirm result
        result = utils.confirm({default = true, description = function ()

            -- get packages for each repositories
            local packages_repo = {}
            local packages_group = {}
            for _, instance in ipairs(packages) do
                -- achive packages by repository
                local reponame = instance:repo() and instance:repo():name() or (instance:is_system() and "system" or "")
                if instance:is_thirdparty() then
                    reponame = instance:name():lower():split("::")[1]
                end
                packages_repo[reponame] = packages_repo[reponame] or {}
                table.insert(packages_repo[reponame], instance)

                -- achive packages by group
                local group = instance:group()
                if group then
                    packages_group[group] = packages_group[group] or {}
                    table.insert(packages_group[group], instance)
                end
            end

            -- show tips
            if opt.toolchain then
                cprint("${bright color.warning}note: ${clear}install or modify (m) these ${bright}toolchain${clear} packages first (pass -y to skip confirm)?")
            else
                cprint("${bright color.warning}note: ${clear}install or modify (m) these packages (pass -y to skip confirm)?")
            end
            for reponame, packages in pairs(packages_repo) do
                if reponame ~= "" then
                    print("in %s:", reponame)
                end
                local packages_showed = {}
                for _, instance in ipairs(packages) do
                    if not packages_showed[tostring(instance)] then
                        local group = instance:group()
                        if group and packages_group[group] and #packages_group[group] > 1 then
                            for idx, package_in_group in ipairs(packages_group[group]) do
                                cprint("  ${yellow}%s${clear} %s %s ${dim}%s", idx == 1 and "->" or "   or",
                                    package_in_group:displayname(), package_in_group:version_str() or "",
                                    package.get_configs_str(package_in_group))
                                for _, tip in ipairs(package_in_group:get("installtips")) do
                                    if idx == 1 then
                                        cprint("     ${yellow}*${clear} %s", tip)
                                    else
                                        cprint("        ${yellow}*${clear} %s", tip)
                                    end
                                end
                                packages_showed[tostring(package_in_group)] = true
                            end
                            packages_group[group] = nil
                        else
                            cprint("  ${yellow}->${clear} %s %s ${dim}%s",
                                instance:displayname(), instance:version_str() or "",
                                package.get_configs_str(instance))
                            for _, tip in ipairs(instance:get("installtips")) do
                                cprint("     ${yellow}*${clear} %s", tip)
                            end
                            packages_showed[tostring(instance)] = true
                        end
                    end
                end
            end
        end, answer = function ()
            cprint("please input: ${bright}y${clear} (y/n/m)")
            io.flush()
            return (io.read() or "false"):trim()
        end})

        -- modify to select 3rd packages?
        if result == "m" then
            packages_modified = _get_confirm_from_3rd(packages)
            result = nil
        else
            -- get confirm result
            result = option.boolean(result)
            if type(result) ~= "boolean" then
                result = true
            end
        end
    end
    return result, packages_modified
end

-- show upgraded packages
function _show_upgraded_packages(packages)
    local upgraded_count = 0
    for _, instance in ipairs(packages) do
        local locked_requireinfo = package.get_locked_requireinfo(instance:requireinfo(), {force = true})
        if locked_requireinfo and locked_requireinfo.version and instance:version() and instance:version():gt(locked_requireinfo.version) then
            cprint("  ${color.dump.string}%s${clear}: %s -> ${color.success}%s", instance:displayname(), locked_requireinfo.version, instance:version_str())
            upgraded_count = upgraded_count + 1
        end
    end
    cprint("${bright}%d packages are upgraded!", upgraded_count)
end

-- fetch packages
function _fetch_packages(packages_fetch, installdeps)

    -- init installed packages
    local packages_fetched = {}
    for _, instance in ipairs(packages_fetch) do
        packages_fetched[tostring(instance)] = false
    end

    -- save terminal mode for stdout, @see https://github.com/xmake-io/xmake/issues/1924
    local term_mode_stdout = tty.term_mode("stdout")

    -- do fetch
    local packages_fetching = {}
    local packages_pending = table.copy(packages_fetch)
    local working_count = 0
    local fetching_count = 0
    local parallelize = true
    runjobs("fetch_packages", function (index)

        -- fetch a new package
        local instance = nil
        while instance == nil and #packages_pending > 0 do
            for idx, pkg in ipairs(packages_pending) do

                -- all dependencies has been fetched? we fetch it now
                local ready = true
                local dep_not_ready = nil
                for _, dep in pairs(installdeps[tostring(pkg)]) do
                    local fetched = packages_fetched[tostring(dep)]
                    if fetched == false then
                        ready = false
                        dep_not_ready = dep
                        break
                    end
                end

                -- get a package with the ready status
                if ready then
                    instance = pkg
                    table.remove(packages_pending, idx)
                    break
                elseif working_count == 0 then
                    if #packages_pending == 1 and dep_not_ready then
                        raise("package(%s): cannot be fetched, there are dependencies(%s) that cannot be fetched!", pkg:displayname(), dep_not_ready:displayname())
                    elseif #packages_pending == 1 then
                        raise("package(%s): cannot be fetched!", pkg:displayname())
                    end
                end
            end
            if instance == nil and #packages_pending > 0 then
                os.sleep(100)
            end
        end
        if instance then

            -- update working count
            working_count = working_count + 1

            -- disable parallelize?
            if not instance:is_parallelize() then
                parallelize = false
            end
            if not parallelize then
                while fetching_count > 0 do
                    os.sleep(100)
                end
            end
            fetching_count = fetching_count + 1

            -- fetch this package
            packages_fetching[index] = instance
            local oldenvs = os.getenvs()
            instance:envs_enter()
            instance:lock()
            instance:fetch()
            instance:unlock()
            os.setenvs(oldenvs)

            -- fix terminal mode to avoid some subprocess to change it
            --
            -- @see https://github.com/xmake-io/xmake/issues/1924
            -- https://github.com/xmake-io/xmake/issues/2329
            if term_mode_stdout ~= tty.term_mode("stdout") then
                tty.term_mode("stdout", term_mode_stdout)
            end

            -- next
            parallelize = true
            fetching_count = fetching_count - 1
            packages_fetching[index] = nil
            packages_fetched[tostring(instance)] = true

            -- update working count
            working_count = working_count - 1
        end
        packages_fetching[index] = nil

    end, {total = #packages_fetch,
          comax = (option.get("verbose") or option.get("diagnosis")) and 1 or 4,
          isolate = true})
end

-- should fetch package?
function _should_fetch_package(instance)
    if instance:is_fetchonly() or not option.get("force") or
        (option.get("shallow") and not instance:is_toplevel()) then
        return true
    end
end

-- should install package?
function _should_install_package(instance)
    _g.package_status_cache = _g.package_status_cache or {}
    local result = _g.package_status_cache[tostring(instance)]
    if result == nil then
        result = package.should_install(instance) or false
        _g.package_status_cache[tostring(instance)] = result
    end
    return result
end

-- do install packages
function _do_install_packages(packages_install, packages_download, installdeps)

    -- we need to hide wait characters if is not a tty
    local show_wait = io.isatty()

    -- init installed packages
    local packages_installed = {}
    for _, instance in ipairs(packages_install) do
        packages_installed[tostring(instance)] = false
    end

    -- save terminal mode for stdout, @see https://github.com/xmake-io/xmake/issues/1924
    local term_mode_stdout = tty.term_mode("stdout")

    -- do install
    local waiting_indicator_helper = show_wait and waiting_indicator.new() or nil
    local packages_installing = {}
    local packages_downloading = {}
    local packages_pending = table.copy(packages_install)
    local packages_in_group = {}
    local working_count = 0
    local installing_count = 0
    local parallelize = true
    runjobs("install_packages", function (index)

        -- fetch a new package
        local instance = nil
        while instance == nil and #packages_pending > 0 do
            for idx, pkg in ipairs(packages_pending) do

                -- all dependencies has been installed? we install it now
                local ready = true
                local dep_not_found = nil
                for _, dep in pairs(installdeps[tostring(pkg)]) do
                    local installed = packages_installed[tostring(dep)]
                    if installed == false or (installed == nil and _should_install_package(dep) and not dep:is_optional()) then
                        ready = false
                        dep_not_found = dep
                        break
                    end
                end
                local group = pkg:group()
                if ready and group then
                    -- this group has been installed? skip it
                    local group_status = packages_in_group[group]
                    if group_status == 1 then
                        table.remove(packages_pending, idx)
                        break
                    -- this group is installing? wait it
                    elseif group_status == 0 then
                        ready = false
                    end
                end

                -- get a package with the ready status
                if ready then
                    instance = pkg
                    table.remove(packages_pending, idx)
                    break
                elseif working_count == 0 then
                    if #packages_pending == 1 and dep_not_found then
                        raise("package(%s): cannot be installed, there are dependencies(%s) that cannot be installed!", pkg:displayname(), dep_not_found:displayname())
                    elseif #packages_pending == 1 then
                        raise("package(%s): cannot be installed!", pkg:displayname())
                    end
                end
            end
            if instance == nil and #packages_pending > 0 then
                os.sleep(100)
            end
        end
        if instance then

            -- update working count
            working_count = working_count + 1

            -- only install the first package in same group
            local group = instance:group()
            if not group or not packages_in_group[group] then

                -- disable parallelize?
                if not instance:is_parallelize() then
                    parallelize = false
                end
                if not parallelize then
                    while installing_count > 0 do
                        os.sleep(100)
                    end
                end
                installing_count = installing_count + 1

                -- mark this group as 'installing'
                if group then
                    packages_in_group[group] = 0
                end

                -- install package from the multiple schemes
                for _, scheme in ipairs(instance:schemes_orderlist()) do
                    instance:prepare_install_scheme(scheme)

                    -- download this package first
                    local downloaded = true
                    if packages_download[tostring(instance)] then
                        packages_downloading[index] = instance
                        packages_installing[index] = nil
                        action_check(instance)
                        downloaded = action_download(instance)
                        packages_downloading[index] = nil
                    end

                    packages_installing[index] = instance
                    if downloaded then
                        if action_install(instance) then
                            -- install ok
                            break
                        end
                    end
                end

                -- reset package status cache
                _g.package_status_cache = nil

                -- register it to local cache if it is root required package
                --
                -- @note we need to register the package in time,
                -- because other packages may be used, e.g. toolchain/packages
                if instance:is_toplevel() then
                    register_packages({instance})
                end

                -- mark this group as 'installed' or 'failed'
                if group then
                    packages_in_group[group] = instance:exists() and 1 or -1
                end

                -- next
                parallelize = true
                installing_count = installing_count - 1
                packages_installing[index] = nil
                packages_installed[tostring(instance)] = true
            end

            -- update working count
            working_count = working_count - 1
        end
        packages_installing[index] = nil
        packages_downloading[index] = nil

    end, {total = #packages_install,
          comax = (option.get("verbose") or option.get("diagnosis")) and 1 or 4,
          isolate = true,
          on_timer = function (running_jobs_indices)

        -- do not print progress info if be verbose
        if option.get("verbose") or not show_wait then
            return
        end

        -- make installing and downloading packages list
        local installing = {}
        local downloading = {}
        for _, index in ipairs(running_jobs_indices) do
            local instance = packages_installing[index]
            if instance then
                table.insert(installing, instance:displayname())
            end
            local instance = packages_downloading[index]
            if instance then
                table.insert(downloading, instance:displayname())
            end
        end
        -- we just return it directly if no thing is waited
        -- @see https://github.com/xmake-io/xmake/issues/3535
        if #installing == 0 and #downloading == 0 then
            return
        end

        -- get waitobjs tips
        local tips = nil
        local waitobjs = scheduler.co_group_waitobjs("install_packages")
        if waitobjs:size() > 0 then
            local names = {}
            for _, obj in waitobjs:keys() do
                if obj:otype() == scheduler.OT_PROC then
                    table.insert(names, obj:name())
                elseif obj:otype() == scheduler.OT_SOCK then
                    table.insert(names, "sock")
                elseif obj:otype() == scheduler.OT_PIPE then
                    table.insert(names, "pipe")
                end
            end
            names = table.unique(names)
            if #names > 0 then
                names = table.concat(names, ",")
                if #names > 16 then
                    names = names:sub(1, 16) .. ".."
                end
                tips = string.format("(%d/%s)", waitobjs:size(), names)
            end
        end

        -- fix terminal mode to avoid some subprocess to change it
        -- @see https://github.com/xmake-io/xmake/issues/1924
        if term_mode_stdout ~= tty.term_mode("stdout") then
            tty.term_mode("stdout", term_mode_stdout)
        end

        -- trace
        waiting_indicator_helper:clear()
        tty.erase_line_to_start().cr()
        cprintf("${yellow}  => ")
        if #downloading > 0 then
            cprintf("downloading ${color.dump.string}%s", table.concat(downloading, ", "))
        end
        if #installing > 0 then
            cprintf("%sinstalling ${color.dump.string}%s", #downloading > 0 and ", " or "", table.concat(installing, ", "))
        end
        cprintf(" .. %s", tips and ("${dim}" .. tips .. "${clear} ") or "")
        waiting_indicator_helper:write()
    end, exit = function(errors)
        if errors then
            tty.erase_line_to_start().cr()
            io.flush()
        end
    end})
end

-- only enable the first package in same group and root packages
function _disable_other_packages_in_group(packages)
    local registered_in_group = {}
    for _, instance in ipairs(packages) do
        local group = instance:group()
        if instance:is_toplevel() and group then
            local required_package = project.required_package(instance:alias() or instance:name())
            if required_package then
                if not registered_in_group[group] and required_package:enabled() then
                    registered_in_group[group] = true
                elseif required_package:enabled() then
                    required_package:enable(false)
                    required_package:save()
                end
            end
        end
    end
end

-- sort packages for installation dependencies
function _sort_packages_for_installdeps(packages, installdeps, order_packages)
    for _, instance in ipairs(packages) do
        local deps = installdeps[tostring(instance)]
        if deps then
            _sort_packages_for_installdeps(deps, installdeps, order_packages)
        end
        table.insert(order_packages, instance)
    end
end

-- get package installation dependencies
function _get_package_installdeps(packages)
    local installdeps = {}
    local packagesmap = {}
    for _, instance in ipairs(packages) do
        -- we need to use alias name first for toolchain/packages
        packagesmap[instance:alias() or instance:name()] = instance
    end
    for _, instance in ipairs(packages) do
        local deps = {}
        if instance:orderdeps() then
            deps = table.copy(instance:orderdeps())
        end
        -- patch toolchain/packages to installdeps, because we need to install toolchain package first
        for _, toolchain in ipairs(instance:toolchains()) do
            for _, packagename in ipairs(toolchain:config("packages")) do
                if packagesmap[packagename] ~= instance then -- avoid loop recursion
                    table.insert(deps, packagesmap[packagename])
                end
            end
        end
        installdeps[tostring(instance)] = deps
    end
    return installdeps
end

-- install packages
function _install_packages(requires, opt)
    opt = opt or {}

    -- load packages
    local packages = package.load_packages(requires, opt)

    -- get package installation dependencies
    local installdeps = _get_package_installdeps(packages)

    -- sort packages for installdeps
    local order_packages = {}
    _sort_packages_for_installdeps(packages, installdeps, order_packages)
    packages = table.unique(order_packages)

    -- save terminal mode for stdout
    local term_mode_stdout = tty.term_mode("stdout")

    -- fetch and register packages (with system) from local first
    local packages_fetch = {}
    for _, instance in ipairs(packages) do
        if _should_fetch_package(instance) then
            table.insert(packages_fetch, instance)
        end
    end
    _fetch_packages(packages_fetch, installdeps)

    -- register all installed root packages to local cache
    register_packages(packages)

    -- filter packages
    local packages_install = {}
    local packages_download = {}
    local packages_unsupported = {}
    local packages_not_found = {}
    local packages_unknown = {}
    for _, instance in ipairs(packages) do
        if _should_install_package(instance) then
            if instance:is_supported() then
                if #instance:urls() > 0 then
                    packages_download[tostring(instance)] = instance
                end
                table.insert(packages_install, instance)
            elseif not instance:is_optional() then
                if not instance:exists() and instance:is_system() then
                    table.insert(packages_unknown, instance)
                else
                    table.insert(packages_unsupported, instance)
                end
            end
        -- @see https://github.com/xmake-io/xmake/issues/2050
        elseif not instance:exists() and not instance:is_optional() then
            local requireinfo = instance:requireinfo()
            if requireinfo and requireinfo.system then
                table.insert(packages_not_found, instance)
            end
        end
    end

    -- exists unknown packages?
    local has_errors = false
    if #packages_unknown > 0 then
        cprint("${bright color.warning}note: ${clear}the following packages were not found in any repository (check if they are spelled correctly):")
        for _, instance in ipairs(packages_unknown) do
            local tips
            local possible_package = package.get_possible_package(instance:name())
            if possible_package then
                tips = string.format(", maybe ${bright}%s %s${clear} in %s", possible_package.name, possible_package.version, possible_package.reponame)
            end
            cprint("  -> %s%s", instance:displayname(), tips or "")
        end
        has_errors = true
    end

    -- exists unsupported packages?
    if #packages_unsupported > 0 then
        local packages_unsupported_maps = {}
        for _, instance in ipairs(packages_unsupported) do
            local key = instance:plat() .. "/" .. instance:arch()
            packages_unsupported_maps[key] = packages_unsupported_maps[key] or {}
            table.insert(packages_unsupported_maps[key], instance)
        end
        for key, instances in pairs(packages_unsupported_maps) do
            cprint("${bright color.warning}note: ${clear}the following packages are unsupported on %s,", key)
            cprint("${clear}you can try running ${bright}`xrepo update-repo`${clear} to update repositories or open a pr to improve these packages:")
            for _, instance in ipairs(instances) do
                cprint("  ${yellow}->${clear} %s %s ${dim}%s",
                    instance:displayname(), instance:version_str() or "", package.get_configs_str(instance))
            end
        end
        has_errors = true
    end

    -- exists not found packages?
    if #packages_not_found > 0 then
        if packages_not_found[1]:is_cross() then
            cprint("${bright color.warning}note: ${clear}system package is not supported for cross-compilation currently, the following system packages cannot be found:")
        else
            cprint("${bright color.warning}note: ${clear}the following packages were not found on your system, try again after installing them:")
        end
        for _, instance in ipairs(packages_not_found) do
            print("  -> %s %s", instance:displayname(), instance:version_str() or "")
        end
        has_errors = true
    end

    if has_errors then
        raise()
    end

    -- get user confirm
    local confirm, packages_modified = _get_confirm(packages_install, opt)
    if not confirm then
        local packages_must = {}
        for _, instance in ipairs(packages_install) do
            if not instance:is_optional() then
                table.insert(packages_must, instance:displayname())
            end
        end
        if #packages_must > 0 then
            raise("packages(%s): must be installed!", table.concat(packages_must, ", "))
        else
            -- continue other actions
            return
        end
    end

    -- show upgraded information
    if option.get("upgrade") then
        print("upgrading packages ..")
    end

    -- some packages are modified? we need to fix packages list and all deps
    if packages_modified then
        order_packages = {}
        _replace_packages(packages, packages_modified)
        installdeps = _get_package_installdeps(packages)
        _sort_packages_for_installdeps(packages, installdeps, order_packages)
        packages = table.unique(order_packages)
     end

    -- sort package urls
    _sort_packages_urls(packages_download)

    -- install all required packages from repositories
    _do_install_packages(packages_install, packages_download, installdeps)

    -- disable other packages in same group
    _disable_other_packages_in_group(packages)

    -- re-register and refresh all root packages to local cache,
    -- because there may be some missing optional dependencies reinstalled
    register_packages(packages)

    -- show upgraded packages
    if option.get("upgrade") then
        _show_upgraded_packages(packages)
    end
    return packages
end

-- install all required packages
--
-- @param requires  the requires table
-- @param opt       the options
--
function main(requires, opt)
    -- we need to install toolchain packages first,
    -- because we will call compiler-specific api in package.on_load,
    -- and we will check package toolchains before calling package.on_load
    --
    -- @see https://github.com/xmake-io/xmake/pull/5466
    local packages = {}
    table.join2(packages, _install_packages(requires, table.join(opt or {}, {toolchain = true})))
    table.join2(packages, _install_packages(requires, opt))

    -- lock packages
    lock_packages(packages)
    return packages
end