Skip to content

Commit 25724f4

Browse files
authored
Implement fpm publish (#876)
* Add fpm_cmd_publish module and add command with --print-request option * Add missing file * Retrieve version * Implement fpm publish --print-package-version * Use show instead of print * Parse license from manifest, add values to JSON and check if allocated * Include token * Include source-path * Use current directory as default source path * Archive package using git archive, determine available archive formats, get system tmp directory, read command line outputs * Fix path, extract name of compressed package, finalize json * Rename --show-request to --show-form-data * Include endpoint and swap base url * Use correct base url, build curl request, fix url * Check for git dependencies * Fix tests * Add docs, fix error * Remove source-path option and add cmd tests * Optionally include token with --show-form-data * Use newest version of toml-f * Add more docs * Simplify version reading * Improve docs * Nit * Fix download bug by removing forward slash * Build model again to obtain dependency tree to make sure there are no git dependencies --------- Co-authored-by: minhqdao <[email protected]>
1 parent 7115629 commit 25724f4

File tree

16 files changed

+334
-37
lines changed

16 files changed

+334
-37
lines changed

app/main.f90

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ program main
99
fpm_install_settings, &
1010
fpm_update_settings, &
1111
fpm_clean_settings, &
12+
fpm_publish_settings, &
1213
get_command_line_settings
1314
use fpm_error, only: error_t
1415
use fpm_filesystem, only: exists, parent_dir, join_path
1516
use fpm, only: cmd_build, cmd_run, cmd_clean
1617
use fpm_cmd_install, only: cmd_install
1718
use fpm_cmd_new, only: cmd_new
1819
use fpm_cmd_update, only : cmd_update
20+
use fpm_cmd_publish, only: cmd_publish
1921
use fpm_os, only: change_directory, get_current_directory
2022

2123
implicit none
@@ -80,6 +82,8 @@ program main
8082
call cmd_update(settings)
8183
type is (fpm_clean_settings)
8284
call cmd_clean(settings)
85+
type is (fpm_publish_settings)
86+
call cmd_publish(settings)
8387
end select
8488

8589
if (allocated(project_root)) then

fpm.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ macros=["FPM_RELEASE_VERSION={version}"]
1111

1212
[dependencies]
1313
toml-f.git = "https://github.com/toml-f/toml-f"
14-
toml-f.rev = "54686e45993f3a9a1d05d5c7419f39e7d5a4eb3f"
14+
toml-f.rev = "d7b892b1d074b7cfc5d75c3e0eb36ebc1f7958c1"
1515
M_CLI2.git = "https://github.com/urbanjost/M_CLI2.git"
1616
M_CLI2.rev = "7264878cdb1baff7323cc48596d829ccfe7751b8"
1717
jonquil.git = "https://github.com/toml-f/jonquil"
18-
jonquil.rev = "05d30818bb12fb877226ce284b9a3a41b971a889"
18+
jonquil.rev = "4c27c8c1e411fa8790dffcf8c3fa7a27b6322273"
1919

2020
[[test]]
2121
name = "cli-test"

src/fpm.f90

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ module fpm
77
fpm_run_settings, fpm_install_settings, fpm_test_settings, &
88
fpm_clean_settings
99
use fpm_dependency, only : new_dependency_tree
10-
use fpm_environment, only: get_env
1110
use fpm_filesystem, only: is_dir, join_path, list_files, exists, &
1211
basename, filewrite, mkdir, run, os_delete_dir
1312
use fpm_model, only: fpm_model_t, srcfile_t, show_model, fortran_features_t, &

src/fpm/cmd/publish.f90

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
!> Upload a package to the registry using the `publish` command.
2+
!>
3+
!> To upload a package you need to provide a token that will be linked to your username and created for a namespace.
4+
!> The token can be obtained from the registry website. It can be used as `fpm publish --token <token>`.
5+
module fpm_cmd_publish
6+
use fpm_command_line, only: fpm_publish_settings
7+
use fpm_manifest, only: package_config_t, get_package_data
8+
use fpm_model, only: fpm_model_t
9+
use fpm_error, only: error_t, fpm_stop
10+
use fpm_versioning, only: version_t
11+
use fpm_filesystem, only: exists, join_path, get_tmp_directory
12+
use fpm_git, only: git_archive, compressed_package_name
13+
use fpm_downloader, only: downloader_t
14+
use fpm_strings, only: string_t
15+
use fpm_settings, only: official_registry_base_url
16+
use fpm, only: build_model
17+
18+
implicit none
19+
private
20+
public :: cmd_publish
21+
22+
contains
23+
24+
!> The `publish` command first builds the root package to obtain all the relevant information such as the
25+
!> package version. It then creates a tarball of the package and uploads it to the registry.
26+
subroutine cmd_publish(settings)
27+
type(fpm_publish_settings), intent(inout) :: settings
28+
29+
type(package_config_t) :: package
30+
type(fpm_model_t) :: model
31+
type(error_t), allocatable :: error
32+
type(version_t), allocatable :: version
33+
type(string_t), allocatable :: form_data(:)
34+
character(len=:), allocatable :: tmpdir
35+
type(downloader_t) :: downloader
36+
integer :: i
37+
38+
call get_package_data(package, 'fpm.toml', error, apply_defaults=.true.)
39+
if (allocated(error)) call fpm_stop(1, '*cmd_build* Package error: '//error%message)
40+
version = package%version
41+
42+
! Build model to obtain dependency tree.
43+
call build_model(model, settings%fpm_build_settings, package, error)
44+
if (allocated(error)) call fpm_stop(1, '*cmd_build* Model error: '//error%message)
45+
46+
!> Checks before uploading the package.
47+
if (.not. allocated(package%license)) call fpm_stop(1, 'No license specified in fpm.toml.')
48+
if (.not. allocated(version)) call fpm_stop(1, 'No version specified in fpm.toml.')
49+
if (version%s() == '0') call fpm_stop(1, 'Invalid version: "'//version%s()//'".')
50+
if (.not. exists('fpm.toml')) call fpm_stop(1, "Cannot find 'fpm.toml' file. Are you in the project root?")
51+
52+
! Check if package contains git dependencies. Only publish packages without git dependencies.
53+
do i = 1, model%deps%ndep
54+
if (allocated(model%deps%dep(i)%git)) then
55+
call fpm_stop(1, "Do not publish packages containing git dependencies. '"//model%deps%dep(i)%name//"' is a git dependency.")
56+
end if
57+
end do
58+
59+
form_data = [ &
60+
string_t('package_name="'//package%name//'"'), &
61+
string_t('package_license="'//package%license//'"'), &
62+
string_t('package_version="'//version%s()//'"') &
63+
& ]
64+
65+
if (allocated(settings%token)) form_data = [form_data, string_t('upload_token="'//settings%token//'"')]
66+
67+
call get_tmp_directory(tmpdir, error)
68+
if (allocated(error)) call fpm_stop(1, '*cmd_publish* Tmp directory error: '//error%message)
69+
call git_archive('.', tmpdir, error)
70+
if (allocated(error)) call fpm_stop(1, '*cmd_publish* Pack error: '//error%message)
71+
form_data = [form_data, string_t('tarball=@"'//join_path(tmpdir, compressed_package_name)//'"')]
72+
73+
if (settings%show_form_data) then
74+
do i = 1, size(form_data)
75+
print *, form_data(i)%s
76+
end do
77+
return
78+
end if
79+
80+
! Make sure a token is provided for publishing.
81+
if (.not. allocated(settings%token)) call fpm_stop(1, 'No token provided.')
82+
83+
call downloader%upload_form(official_registry_base_url//'/packages', form_data, error)
84+
if (allocated(error)) call fpm_stop(1, '*cmd_publish* Upload error: '//error%message)
85+
end
86+
end

src/fpm/dependency.f90

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -668,8 +668,8 @@ subroutine get_from_registry(self, target_dir, global_settings, error, downloade
668668

669669
! Define location of the temporary folder and file.
670670
tmp_pkg_path = join_path(global_settings%path_to_config_folder, 'tmp')
671-
tmp_pkg_file = join_path(tmp_pkg_path, 'package_data.tmp')
672671
if (.not. exists(tmp_pkg_path)) call mkdir(tmp_pkg_path)
672+
tmp_pkg_file = join_path(tmp_pkg_path, 'package_data.tmp')
673673
open (newunit=unit, file=tmp_pkg_file, action='readwrite', iostat=stat)
674674
if (stat /= 0) then
675675
call fatal_error(error, "Error creating temporary file for downloading package '"//self%name//"'."); return
@@ -697,7 +697,6 @@ subroutine get_from_registry(self, target_dir, global_settings, error, downloade
697697
if (is_dir(cache_path)) call os_delete_dir(os_is_unix(), cache_path)
698698
call mkdir(cache_path)
699699

700-
print *, "Downloading '"//join_path(self%namespace, self%name, version%s())//"' ..."
701700
call downloader%get_file(target_url, tmp_pkg_file, error)
702701
if (allocated(error)) then
703702
close (unit, status='delete'); return
@@ -782,7 +781,7 @@ subroutine check_and_read_pkg_data(json, node, download_url, version, error)
782781
call fatal_error(error, "Failed to read download url for '"//join_path(node%namespace, node%name)//"'."); return
783782
end if
784783

785-
download_url = official_registry_base_url//'/'//download_url
784+
download_url = official_registry_base_url//download_url
786785

787786
if (.not. q%has_key('version')) then
788787
call fatal_error(error, "Failed to download '"//join_path(node%namespace, node%name)//"': No version found."); return

src/fpm/downloader.f90

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module fpm_downloader
33
use fpm_filesystem, only: which
44
use fpm_versioning, only: version_t
55
use jonquil, only: json_object, json_value, json_error, json_load, cast_to_object
6+
use fpm_strings, only: string_t
67

78
implicit none
89
private
@@ -12,12 +13,12 @@ module fpm_downloader
1213
!> This type could be entirely avoided but it is quite practical because it can be mocked for testing.
1314
type downloader_t
1415
contains
15-
procedure, nopass :: get_pkg_data, get_file, unpack
16+
procedure, nopass :: get_pkg_data, get_file, upload_form, unpack
1617
end type
1718

1819
contains
1920

20-
!> Perform an http get request and save output to file.
21+
!> Perform an http get request, save output to file, and parse json.
2122
subroutine get_pkg_data(url, version, tmp_pkg_file, json, error)
2223
character(*), intent(in) :: url
2324
type(version_t), allocatable, intent(in) :: version
@@ -51,6 +52,7 @@ subroutine get_pkg_data(url, version, tmp_pkg_file, json, error)
5152
json = ptr
5253
end
5354

55+
!> Download a file from a url using either curl or wget.
5456
subroutine get_file(url, tmp_pkg_file, error)
5557
character(*), intent(in) :: url
5658
character(*), intent(in) :: tmp_pkg_file
@@ -59,10 +61,10 @@ subroutine get_file(url, tmp_pkg_file, error)
5961
integer :: stat
6062

6163
if (which('curl') /= '') then
62-
print *, "Downloading package data from '"//url//"' ..."
64+
print *, "Downloading '"//url//"' -> '"//tmp_pkg_file//"'"
6365
call execute_command_line('curl '//url//' -s -o '//tmp_pkg_file, exitstat=stat)
6466
else if (which('wget') /= '') then
65-
print *, "Downloading package data from '"//url//"' ..."
67+
print *, "Downloading '"//url//"' -> '"//tmp_pkg_file//"'"
6668
call execute_command_line('wget '//url//' -q -O '//tmp_pkg_file, exitstat=stat)
6769
else
6870
call fatal_error(error, "Neither 'curl' nor 'wget' installed."); return
@@ -73,6 +75,33 @@ subroutine get_file(url, tmp_pkg_file, error)
7375
end if
7476
end
7577

78+
!> Perform an http post request with form data.
79+
subroutine upload_form(endpoint, form_data, error)
80+
character(len=*), intent(in) :: endpoint
81+
type(string_t), intent(in) :: form_data(:)
82+
type(error_t), allocatable, intent(out) :: error
83+
84+
integer :: stat, i
85+
character(len=:), allocatable :: form_data_str
86+
87+
form_data_str = ''
88+
do i = 1, size(form_data)
89+
form_data_str = form_data_str//"-F '"//form_data(i)%s//"' "
90+
end do
91+
92+
if (which('curl') /= '') then
93+
print *, 'Uploading package ...'
94+
call execute_command_line('curl -X POST -H "Content-Type: multipart/form-data" ' &
95+
& //form_data_str//endpoint, exitstat=stat)
96+
else
97+
call fatal_error(error, "'curl' not installed."); return
98+
end if
99+
100+
if (stat /= 0) then
101+
call fatal_error(error, "Error uploading package to registry."); return
102+
end if
103+
end
104+
76105
!> Unpack a tarball to a destination.
77106
subroutine unpack(tmp_pkg_file, destination, error)
78107
character(*), intent(in) :: tmp_pkg_file

src/fpm/error.f90

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ subroutine fpm_stop(value,message)
171171
flush(unit=stderr,iostat=iostat)
172172
flush(unit=stdout,iostat=iostat)
173173
if(value>0)then
174-
write(stderr,'("<ERROR>",a)')trim(message)
174+
write(stderr,'("<ERROR> ",a)')trim(message)
175175
else
176176
write(stderr,'("<INFO> ",a)')trim(message)
177177
endif

src/fpm/git.f90

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
!> Implementation for interacting with git repositories.
22
module fpm_git
33
use fpm_error, only: error_t, fatal_error
4-
use fpm_filesystem, only : get_temp_filename, getline, join_path
4+
use fpm_filesystem, only : get_temp_filename, getline, join_path, execute_and_read_output
55
implicit none
66

7-
public :: git_target_t
8-
public :: git_target_default, git_target_branch, git_target_tag, &
9-
& git_target_revision
10-
public :: git_revision
11-
public :: git_matches_manifest
12-
public :: operator(==)
13-
7+
public :: git_target_t, git_target_default, git_target_branch, git_target_tag, git_target_revision, git_revision, &
8+
& git_archive, git_matches_manifest, operator(==), compressed_package_name
9+
10+
!> Name of the compressed package that is generated temporarily.
11+
character(len=*), parameter :: compressed_package_name = 'compressed_package'
1412

1513
!> Possible git target
1614
type :: enum_descriptor
@@ -307,5 +305,33 @@ subroutine info(self, unit, verbosity)
307305

308306
end subroutine info
309307

308+
!> Archive a folder using `git archive`.
309+
subroutine git_archive(source, destination, error)
310+
!> Directory to archive.
311+
character(*), intent(in) :: source
312+
!> Destination of the archive.
313+
character(*), intent(in) :: destination
314+
!> Error handling.
315+
type(error_t), allocatable, intent(out) :: error
316+
317+
integer :: stat
318+
character(len=:), allocatable :: cmd_output, archive_format
319+
320+
call execute_and_read_output('git archive -l', cmd_output, error)
321+
if (allocated(error)) return
322+
323+
if (index(cmd_output, 'tar.gz') /= 0) then
324+
archive_format = 'tar.gz'
325+
else
326+
call fatal_error(error, "Cannot find a suitable archive format for 'git archive'."); return
327+
end if
328+
329+
call execute_command_line('git archive HEAD --format='//archive_format//' -o '// &
330+
& join_path(destination, compressed_package_name), exitstat=stat)
331+
if (stat /= 0) then
332+
call fatal_error(error, "Error packing '"//source//"'."); return
333+
end if
334+
end
335+
310336

311337
end module fpm_git

src/fpm/manifest/package.f90

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ module fpm_manifest_package
8080
!> Fortran meta data
8181
type(fortran_config_t) :: fortran
8282

83+
!> License meta data
84+
character(len=:), allocatable :: license
85+
8386
!> Library meta data
8487
type(library_config_t), allocatable :: library
8588

@@ -151,6 +154,8 @@ subroutine new_package(self, table, root, error)
151154
return
152155
endif
153156

157+
call get_value(table, "license", self%license)
158+
154159
if (len(self%name) <= 0) then
155160
call syntax_error(error, "Package name must be a non-empty string")
156161
return

src/fpm/manifest/test.f90

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
!>[test.dependencies]
1616
!>```
1717
module fpm_manifest_test
18-
use fpm_manifest_dependency, only : dependency_config_t, new_dependencies
18+
use fpm_manifest_dependency, only : new_dependencies
1919
use fpm_manifest_executable, only : executable_config_t
2020
use fpm_error, only : error_t, syntax_error, bad_name_error
2121
use fpm_toml, only : toml_table, toml_key, toml_stat, get_value, get_list

0 commit comments

Comments
 (0)